blob: 90a87b9e1e1314bdd26df6bab865955d32cc7147 [file]
.. _nuttx-tasking:
=============
NuttX Tasking
=============
An RTOS as a library
====================
What is an RTOS? NuttX, as with all RTOSs, is a collection of various features
bundled as a library. It does not execute except when either:
1. The application calls into the NuttX library code, OR
2. An interrupt occurs.
There is no meaningful way to represent an architecture that is implemented
as a library of user managed functions with a diagram.
You can however, pick any subsystem of an RTOS and represent
that in some fashion.
Kernel Threads
==============
There are some RTOS functions that are implemented by internal threads,
for instance :ref:`kernel-threads-vs-pthreads`, :ref:`tasks-vs-threads`,
:ref:`kernel-modules`.
.. todo:: Provide more content here :-)
The Scheduler
=============
Schedulers and Operating Systems
--------------------------------
An operating system is a complete environment for developing applications.
One important component of an operating system is the scheduler.
That logic that controls when tasks or threads execute.
Actually, more than that; the scheduler really determines what a task
or a thread is! Most tiny operating systems are really not operating
systems in the sense of providing a complete operating environment.
Rather these tiny operating systems consist really only of a scheduler.
That is how important the scheduler is.
Task Control Block (TCB)
------------------------
In NuttX a thread is any controllable sequence of instruction execution
that has its own stack.
Each task is represented by a data structure called a task control block
or TCB. That data structure is defined in the header file
``include/nuttx/sched.h``.
Task Lists
----------
These TCBs are retained in lists. The state of a task is indicated both
by the ``task_state`` field of the TCB and by a series of task lists.
Although it is not always necessary, most of these lists are prioritized
so that common list handling logic can be used (only the ``g_readytorun``,
the ``g_pendingtasks``, and the ``g_waitingforsemaphore`` lists
need to be prioritized).
All new tasks start in an initial, non-running state:
.. code-block:: c
volatile dq_queue_t g_inactivetasks;
* This is the list of all tasks that have been initialized, but not yet
activated. NOTE: This is the only list that is not prioritized.
* When the task is initialized, it is moved to a ready-to-run list.
There are two lists representing ready-to-run threads and several
lists representing blocked threads. Here are the ready-to-run threads:
.. code-block:: c
volatile dq_queue_t g_readytorun;
* This is the list of all tasks that are ready to run.
The head of this list is the currently active task;
the tail of this list is always the idle task.
.. code-block:: c
volatile dq_queue_t g_pendingtasks;
* This is the list of all tasks that are ready-to-run, but cannot be placed
in the ``g_readytorun`` list because:
1. They are higher priority than the currently active task at the head
of the ``g_readytorun`` list, AND
2. the currently active task has disabled pre-emption.
These tasks will stay in this holding list until pre-emption is again
enabled (or until the currently active task voluntarily relinquishes
the CPU).
* Tasks in the ``g_readytorun`` list may become blocked.
In this cased, their TCB will be moved to one of the blocked lists.
When the block task is ready-to-run, its TCB will be moved back to either
the ``g_readytorun`` to ``the g_pendingtasks`` lists, depending up
if pre-emption is disabled and upon the priority of the tasks.
Here are the blocked task lists:
.. code-block:: c
volatile dq_queue_t g_waitingforsemaphore;
* This is the list of all tasks that are blocked waiting for a semaphore.
.. code-block:: c
volatile dq_queue_t g_waitingforsignal;
* This is the list of all tasks that are blocked waiting for a signal
(only if signal support has not been disabled).
.. code-block:: c
volatile dq_queue_t g_waitingformqnotempty;
* This is the list of all tasks that are blocked waiting for a message queue
to become non-empty (only if message queue support has not been disabled).
.. code-block:: c
volatile dq_queue_t g_waitingformqnotfull;
* This is the list of all tasks that are blocked waiting for a message queue
to become non-full (only if message queue support has not been disabled).
.. code-block:: c
volatile dq_queue_t g_waitingforfill;
* This is the list of all tasks that are blocking waiting for a page fill
(only if on-demand paging is selected).
Reference: ``nuttx/sched/sched/sched.h``.
State Transition Diagram
========================
The state of a thread can then be easily represented with this simple state
transition diagram.
.. todo:: Provide State Transition Diagram.
Scheduling Policies
===================
In order to be a real-time OS, an RTOS must support ``SCHED_FIFO``.
That is, strict priority scheduling. The thread with the highest priority
runs.. Period. The thread with the highest priority is always associated
with the TCB at the head of the ``g_readytorun`` list.
NuttX supports one additional real-time scheduling policy: ``SCHED_RR``.
The RR stands for **round-robin** and this is sometimes called
**round-robin scheduling**. In this case, NuttX supports timeslicing.
If a task with ``SCHED_RR`` scheduling policy is running, then when each
timeslice elapses, it will give up the CPU to the next task that is
at the same priority.
.. note::
1. If there is only one task at this priority, ``SCHED_RR`` and
``SCHED_FIFO`` are the same, AND
2. ``SCHED_FIFO`` tasks are never pre-empted in this way.
Task IDs
========
Each task is represented not only by a TCB but also by a numeric task ID.
Given a task ID, the RTOS can find the TCB.
Given a TCB, the RTOS can find the task ID.
So they are functionally equivalent.
Only the task ID, however, is exposed at the RTOS/application interfaces.
NuttX Tasks
===========
Processes vs. Threads
---------------------
In larger system OS such as BSD, Linux, or Windows you will often hear
the name process used to refer to threads managed by the OS.
A process is more than a thread as we have been discussing so far.
A process is a protected environment that hosts one or more threads.
By environment we mean the set of resources set aside by the OS but
in the case of the protected environment of the process we are specifically
referring its address space.
.. note::
In order to implement the process' address space, the CPU must support
a memory management unit (MMU).
**The MMU is used to enforce the protected process environment.**
However, NuttX was designed to support the more resource constrained,
lower-end, deeply embedded MCUs. Those MCUs seldom have an MMU and,
as a consequence, can never support processes as are supported by BSD, Linux,
or Windows.
.. important:: NuttX does not support processes.
NuttX will support an MMU but it will not use the MMU to support processes.
NuttX operates only in a flat address space.
NuttX will use the MMU to control the instruction and data caches and
to support protected memory regions.
This may change in future, but this is how things are right now.
NuttX Tasks and Task Resources
------------------------------
All RTOSs support the notion of a task. A task is the RTOS's moral equivalent
of a process. Like a process, a task is a thread with an environment
associated with it.
This environment is like environment of the process but does not include
a private address space.
This environment is private and unique to a task.
Each task has its own environment.
This task environment consists of a number of resources
(as represented in the TCB). Of interest in this discussion are the following.
Note that any of these task resources may be disabled in the NuttX
configuration to reduce the NuttX memory footprint:
1. **Environment Variables**. This is the collection of variable assignments
of the form: ``VARIABLE=VALUE``.
2. **File Descriptors**. A file descriptor is a task specific number
that represents an open resource (a file or a device driver, for example).
3. **Sockets**. A socket descriptor is like a file descriptor, but
the open resource in this case is a network socket.
4. **Streams**. Streams represent standard C buffered I/O.
Streams wrap file descriptors or sockets to provide a new set of interface
functions for dealing with the standard C I/O (like ``fprintf()``,
``fwrite()``, etc.).
In NuttX, a task is created using the interface ``task_create()``.
NuttX Task Exit Sequence
------------------------
.. figure:: task_exit_sequence.png
:alt: Task Exit Sequence diagram.
Task Exit Sequence diagram.
The Pseudo File System and Device Drivers
=========================================
A full discussion of the NuttX file system belongs elsewhere,
see :ref:`nuttx-filesystem` for more details.
But in order to talk about task resources, we also need to have
a little knowledge of the NuttX file system.
NuttX implements a Virtual Files System (VFS) that may be used to communicate
with a number of different entities via the standard ``open()``, ``close()``,
``read()``, ``write()``, etc, interfaces.
Like other VFSs, the NuttX VFS will support file system mount points,
files, directories, device drivers, etc.
Also, as with other VFSs, the NuttX file system will support
pseudo-file systems, that is, file systems that appear as normal media
but are really presented under programmatic control.
In Linux, for example, you have the ``/proc`` and the ``/sys``
psuedo-file systems.
There is no physical media underlying the pseudo-file system.
The NuttX root file system is always a psuedo-file system.
This is just the opposite from Linux. With Linux the root file system
must always be some physical block device (if only an initrd ram disk).
Then once you have mounted the physical root file system, you can mount
other file systems including Linux pseudo-filesystems like ``/proc``
or ``/sys``.
With NuttX, the root file system is always
a pseudo-file system that does not require any underlying block driver
or physical device.
Then you can mount real filesystem in the pseudo-filesystem.
This arrangement makes life much easier for the tiny embedded world (but also
has a few limitations like where you can mount file systems).
**NuttX interacts with devices via device drivers** that is via software
that controls hardware and conforms to certain NuttX conventions
(see ``include/nuttx/fs/fs.h``). Device drivers are represented
by device nodes in the pseudo-file system.
By convention, these device nodes are created in the ``/dev`` directory.
Now that we have digressed a little to introduce the NuttX file system
and device nodes, we can return to our discussion of task resources.
``/dev/console`` and Standard Streams
-------------------------------------
There are three special cases of I/O: ``stdin``, ``stdout``, and ``stderr``.
These are type ``FILE*`` and correspond to file descriptors ``0``, ``1``,
and ``2`` respectively.
When the very first thread is created (called the IDLE thread),
the special device node ``/dev/console`` is opened. ``/dev/console`` provides
the ``stdin``, ``stdout``, and ``stderr`` for the initial task.
Inheritance of the Task Environment and I/O Redirection
=======================================================
When one task creates a new task, that new task inherits the task resources
of its parent. This includes all of the environment variables,
file descriptors, and sockets.
.. note::
Task resources inheritance can be limited by special options
in the NuttX configuration.
So, if nothing special is done, then every task will use ``/dev/console``
for the standard I/O. However, a task may close file descriptor
``0`` through ``2`` and open a new device for standard I/O.
Then any children tasks that are created will inherit that new re-directed
standard I/O as well.
This mechanism is used throughout NuttX.
For example in the THTTPD server to redirect socket I/O to standard I/O
for CGI tasks. In the Telnet server so that new tasks inherit the
Telnet session.
Tasks vs. Pthreads
==================
Systems like Linux also support POSIX pthreads.
In the Linux environment, the process is created with one thread running
in it. But by using interfaces like ``pthread_create()``, you can create
multiple threads that run and share the same process resources.
NuttX also supports POSIX pthreads and the NuttX pthreads also support
this behavior. That is, the NuttX POSIX pthreads also share the resources
of the parent task.
However, since NuttX does not support process address environments,
the difference is not so striking.
When a task creates a pthread, the newly create pthread will share
the environment variables, file descriptors, sockets, and streams
of the parent task.
.. note::
Task resources are reference counted and will persist
as long as a thread in the task group is still active.
See :ref:`tasks-vs-threads` for more details.
Process IDs / Task IDs / Pthread IDs
====================================
The term process ID is standard (usually abbreviated as pid) and used to
identify a task in NuttX. So, more technically, this number is a task ID
as was described above.
Pthreads are also described by a ``pthread_t`` ID.
In NuttX, the ``pthread_t`` ID is also the same task ID.