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>
1000 lines
40 KiB
ReStructuredText
1000 lines
40 KiB
ReStructuredText
.. _memory-configurations:
|
||
|
||
=====================
|
||
Memory Configurations
|
||
=====================
|
||
|
||
.. _flat-build:
|
||
|
||
Flat, Embedded Build
|
||
====================
|
||
|
||
The normal build of NuttX for the typical embedded environment uses
|
||
a single blob of code in a flat address space.
|
||
For most lower end CPUs (such as the ARM Cortex-M family),
|
||
this means executing directly out of the physical address space.
|
||
|
||
Even if the CPU has an MMU (such as with the ARM Cortex-A family),
|
||
the typical NuttX build still uses a flat address space with the MMU
|
||
providing only an identity mapping.
|
||
|
||
In this case, there is still benefit from using the MMU because
|
||
the MMU provides fine control over caching and memory behavior
|
||
over the address space.
|
||
|
||
|
||
.. _on-demand-paging:
|
||
|
||
On-Demand Paging
|
||
================
|
||
|
||
NuttX also supports on-demand paging via ``CONFIG_PAGING``.
|
||
On-demand paging is a method of virtual memory management and requires
|
||
the the CPU architecutre support a MMU.
|
||
|
||
In a system that uses on-demand paging, the OS responds to a page fault
|
||
by copying data from some storage media into physical memory and setting up
|
||
the MMU to provided the necessary virtual address mapping.
|
||
The CPU can then continue from the page fault with the necessary memory
|
||
in place for the virtual address.
|
||
|
||
Execution Image
|
||
---------------
|
||
|
||
The execution image is still built as one blob and appears as one blob on the
|
||
storage media. But the execution image is paged into arbitrary physical
|
||
addresses with non-contiguous virtual addresses.
|
||
The physical and virtual address spaces are then "checker boards"
|
||
of memory in use.
|
||
|
||
Advantages
|
||
----------
|
||
|
||
The main advantage of on-demand paging is that you can execute a single
|
||
program that is much larger than the physical address space or a collection
|
||
of programs that together are much larger than the physical address space.
|
||
|
||
Current Implementation
|
||
----------------------
|
||
|
||
On-demand paging is currently implemented only for the NXP LPC31xx family.
|
||
The LPC31xx has a 192KiB internal SRAM and with on-demand paging the LPC31xx
|
||
can execute a huge program residing in SerialFLASH by bringing in new pages
|
||
as needed from the SerialFLASH when ``_page`` ``fault_s`` occur.
|
||
|
||
|
||
.. _protected-build:
|
||
|
||
Protected Build
|
||
===============
|
||
|
||
Protected Build Mode
|
||
--------------------
|
||
|
||
NuttX also supports a protected build mode for certain CPU architectures
|
||
if ``CONFIG_BUILD_PROTECTED`` is selected.
|
||
|
||
**In this mode, NuttX is built as two blobs, one privileged and
|
||
one unprivileged.** The privileged blob contains the RTOS, and the other,
|
||
unprivileged blob holds all of the applications.
|
||
|
||
The build supports system calls via a call gate so that the unprivileged,
|
||
application code can access the privileged RTOS services.
|
||
|
||
Memory Protection
|
||
-----------------
|
||
|
||
Within each blob, the address space is flat. No MMU is required to support
|
||
the protected build since no address mapping is performed.
|
||
|
||
In fact, this feature is currently available only for the ARM Cortex-M family.
|
||
In this case the Cortex-M's MPU provides the security in the address spaces
|
||
of the two blobs.
|
||
|
||
This feature could also be implemented with a CPU that supports an MMU,
|
||
but there has thus far been no reason to implement such a configuration.
|
||
|
||
Dynamic Memory Allocation
|
||
-------------------------
|
||
|
||
The purpose of protected build then is focused primarily on securing the OS
|
||
and CPU resources from potential rogue applications.
|
||
|
||
The MPU simply protects the hardware, code regions, and data regions
|
||
of the RTOS. But dynamic memory allocations become more complex.
|
||
Protection is also required for (certain) memory allocations made by the RTOS.
|
||
The RTOS must also be capable of allocating memory that is accessible
|
||
by user applications (such as the user thread stacks).
|
||
|
||
Dual Heaps
|
||
----------
|
||
|
||
In systems with MMUs, the privilege of each page of memory can be controlled
|
||
and there are established architectures for memory management of processes
|
||
(see below). However, with only an MPU with a limited number of pages
|
||
(the Cortex-M has 8 pages only!) we are forced to resolve this problem
|
||
by dividing available heap memory into two heaps:
|
||
a privileged heap and an unprivileged heap, using different allocations
|
||
mechanisms for each (kmalloc and malloc, respectively).
|
||
|
||
|
||
.. _addrenv:
|
||
|
||
Address Environments
|
||
====================
|
||
|
||
If the option ``CONFIG_ARCH_ADDRENV`` is selected, then NuttX will support
|
||
address environments in the following way: the base code is still one blob
|
||
and identical in every way to the "Flat Embedded Build" discussed above.
|
||
But all applications are loaded into RAM from executable files,
|
||
separately compiled and separately linked programs, that reside
|
||
in a file system.
|
||
|
||
Instead of starting the user application at a fixed, in-memory address
|
||
(such as ``nsh_main()``), the system will start the program contained
|
||
in an executable file, given its path.
|
||
|
||
That initial user program can then start additional applications
|
||
from executable files in a file system.
|
||
|
||
Per Program
|
||
-----------
|
||
|
||
As each program is started, a new address environment is created
|
||
for the new task. This address environment is then unique for each task.
|
||
A task does not have the capability to access just anything in the address
|
||
environment. A task may only access addresses within its own address space
|
||
and within the address space of the base code.
|
||
|
||
MMU (Memory Management Unit)
|
||
----------------------------
|
||
|
||
The CPU must support an MMU in order to provide address environments.
|
||
|
||
This feature was originally implemented to support the ZiLOG Z180 which
|
||
is an 8-bit CPU (basically a Z80) that also supports a simple MMU.
|
||
Specifically for the P112 platform. Unfortunately, due to complex tool
|
||
issues and fading interest, that port was fully implemented but never tested.
|
||
|
||
As of this writing, the implementation of address environment support
|
||
for the Cortex-A family is complete and verified.
|
||
An example configuration is available at
|
||
``nuttx/boards/arm/sama5/sama5d4-ek/configs/elf``.
|
||
|
||
|
||
.. _kernel-build:
|
||
|
||
Kernel Build
|
||
============
|
||
|
||
The Kernel Build
|
||
----------------
|
||
|
||
And finally, there is the kernel build that is enabled with
|
||
``CONFIG_BUILD_KERNEL=y``.
|
||
The NuttX kernel build mode is similar to building with address environments:
|
||
|
||
* Each application process executes from its own, private address environment.
|
||
|
||
But, in addition, there are some features similar to the protected build mode:
|
||
|
||
* NuttX is built as a monolithic kernel, similar to the way that NuttX
|
||
is built in the Protected Build Mode.
|
||
* All of the code that executes within the kernel executes in privileged,
|
||
kernel mode. Again, this is analogous to the Protected Build Mode.
|
||
* All user applications are executed with their own private
|
||
address environments in unprivileged, user-mode.
|
||
|
||
MMU Required
|
||
------------
|
||
|
||
In order to support this kernel build mode, the **processor must provide
|
||
a Memory Management Unit (MMU)**. The MMU is used to provide both the address
|
||
environment for the user application as well as to enforce
|
||
the user-/kernel-mode privileges.
|
||
|
||
This kernel build feature has been fully implemented and verified
|
||
on the Cortex-A family of processes.
|
||
A functioning example can be found at
|
||
``nuttx/boards/arm/sama5/sama5d4-ek/configs/knsh``.
|
||
|
||
Process Environment
|
||
-------------------
|
||
|
||
Such user applications that execute their own private, unprivileged address
|
||
environments are usually referred to as processes.
|
||
|
||
The ``CONFIG_BUILD_KERNEL=y`` build is the first step toward support
|
||
for processes in NuttX.
|
||
|
||
|
||
The Roadmap Toward Processes
|
||
============================
|
||
|
||
Processes
|
||
---------
|
||
|
||
Let's call these programs with their own address spaces processes.
|
||
The term process may bring along some additional baggage and imply more than
|
||
is intended, at least in the short term.
|
||
Conceptually, processes are a natural extension of what is referred
|
||
as a task group in NuttX parlance.
|
||
|
||
|
||
Binding to the Base Code
|
||
------------------------
|
||
|
||
So how does the process communicate with the base code?
|
||
There are two ways now. Both are fully implemented, either would work:
|
||
|
||
Symbol Tables
|
||
-------------
|
||
|
||
In this case, the object file is only partially linked. It contains references
|
||
to undefined external symbols. In this case, the base code must provide
|
||
a symbol table, that is, a table that maps a symbol to its address
|
||
in the base code. The NuttX dynamic loader (``binfmt/``) will automatically
|
||
link the the program to base code using this symbol table when the program
|
||
is loaded from the file system into RAM.
|
||
|
||
Symbol Table Helpers
|
||
^^^^^^^^^^^^^^^^^^^^
|
||
|
||
NuttX provides several helpers to make dealing with symbol tables exported
|
||
from the base code less painful.
|
||
|
||
All base code symbols are maintained in comma-separated-value (CSV) files.
|
||
There are three: ``nuttx/libc/libc.csv``, ``nuttx/libc/math.csv``, and
|
||
``nuttx/syscall/syscall.csv``.
|
||
These CSV files contain descriptions of all symbols that could be exported
|
||
by the base code.
|
||
Then there is the program at ``nuttx/tools/mksymtab.c`` that can be used
|
||
to generate symbol tables from these CSV files.
|
||
|
||
Call Gates
|
||
^^^^^^^^^^
|
||
|
||
A second way for the program loaded into memory to communicate with the
|
||
base code is via a call gate, i.e., via system calls.
|
||
|
||
A call gate is normally used to change the privilege level of the thread
|
||
when calling into system code.
|
||
However, the same system call mechanism can be used to simply call into
|
||
the OS without having any a priori knowledge of the address of the RTOS
|
||
service in the base code.
|
||
Thus, the symbol tables could be eliminated completely at least for the case
|
||
of OS system calls (the C library is another story).
|
||
|
||
Status
|
||
^^^^^^
|
||
|
||
Both the symbol table logic and the system call logic are already fully
|
||
implemented and verified and fully integrated with address environments
|
||
up to this point.
|
||
|
||
A sample configuration that does all of these things is here:
|
||
``nuttx/boards/arm/sama5/sama5d4-ek/configs/elf`` and additional information
|
||
is available in ``nuttx/boards/arm/sama5/sama5d4-ek/README.txt``.
|
||
|
||
|
||
Protection and Privileges
|
||
-------------------------
|
||
|
||
Protection
|
||
^^^^^^^^^^
|
||
|
||
One of the greatest benefits of processes, however, is the security that they
|
||
can provide. As described above, on program cannot access memory resources
|
||
of any other program because those resources lie in a different address
|
||
environment.
|
||
|
||
But none of the address space outside of the program private address
|
||
environment is protected:
|
||
The base code and its private memory are not protected;
|
||
the hardware is not protected;
|
||
and anything allocated from the heap is not protected.
|
||
|
||
So a misbehaving, rogue program can still crash the system or corrupt
|
||
the stack or memory allocation made by other programs.
|
||
|
||
MMU Protection
|
||
^^^^^^^^^^^^^^
|
||
|
||
Of course, the MMU can also be used to protect the resources
|
||
outside of programs address environment. It would be a simple matter
|
||
to protect the hardware and the base code memory resources from programs.
|
||
|
||
The base code would run in privileged mode with full access;
|
||
the user applications would run in unprivileged mode and only have the ability
|
||
to access memory resources within their own address environment.
|
||
This use of the MMU, however, raises some additional issues:
|
||
|
||
No Symbol Tables
|
||
^^^^^^^^^^^^^^^^
|
||
|
||
Symbol Tables could not be used in such a protected environment to call
|
||
into the base code. Only system calls could be supported. These call gates
|
||
could switch to privileged mode temporarily in order execute the RTOS service,
|
||
then return to unprivileged mode when returning to the program.
|
||
|
||
|
||
The Line
|
||
--------
|
||
|
||
.. attention:: Let's draw a line right here.
|
||
|
||
Everything above this point is history and just a summary of the way
|
||
things are. Every thing below this point is a roadmap that I will be
|
||
following in the further development of these features.
|
||
Some bits and pieces and been implemented and indicated.
|
||
|
||
So continuing from the other side of this line...
|
||
|
||
|
||
File Descriptors
|
||
----------------
|
||
|
||
In the FLAT and PROTECTED builds, the file descriptors are maintained
|
||
in a table internal to the OS.
|
||
|
||
In PROTECTED mode, this requires a system call to access the file descriptors.
|
||
In the KERNEL build mode, however, this system call overhead is wasteful.
|
||
Ideally, the file descriptor table would be moved out of the OS and into the
|
||
process address space where it can be accessed directly.
|
||
|
||
|
||
Overlapped Address Spaces
|
||
-------------------------
|
||
|
||
Currently, the user-process virtual address space and the kernel-mode
|
||
virtual address space do not overlapped.
|
||
This is an odd arrangement and forces the user address space into awkward
|
||
regions. This was done so that a single address environment can support
|
||
both user- and kernel- mode operation.
|
||
|
||
More correctly, the user address space should include the entire virtual
|
||
address space (other that regions that may have specific hardware
|
||
functionality such as vector tables).
|
||
And the user address space should overlap the kernel address space.
|
||
|
||
Supporting such overlapping address spaces would require to changes
|
||
to the currently MMU handling:
|
||
|
||
* First, on entry into a kernel mode system call, MMU mapping of the user
|
||
address space must be disabled so that the kernel address space
|
||
is accessible, and
|
||
* When pointer parameters are passed to the OS, these will be references
|
||
to user space data. The user-address space must be re-established prior
|
||
to a accessing the user-space data passed with the system call.
|
||
* Care must be taken in general when interacting with any user-space
|
||
resource, memory, callbacks, etc., to assure that the correct address
|
||
environment is in place. Many places now assume that that is true.
|
||
|
||
|
||
Libraries
|
||
---------
|
||
|
||
But not all of the symbols that might be exported from the base code map
|
||
to system calls. Many of the NuttX facilities operate simply in user mode:
|
||
Think of ``strlen()``, ``printf()``, ``rand()``, etc.
|
||
|
||
With this strict enforcement of address spaces, the only way that these
|
||
addition functions can be called is if they are brought into the same
|
||
address space as the program.
|
||
|
||
NuttX Libraries
|
||
^^^^^^^^^^^^^^^
|
||
|
||
User programs are separately compiled and separately linked.
|
||
NuttX currently builds all of the user callable functions into static
|
||
libraries. So, as part of their build process, user programs can simply
|
||
link with these static libraries to include to bring the callable code into
|
||
the address space of each program.
|
||
|
||
Libraries created by NuttX include: ``libsyscall.a`` that holds the system
|
||
call "proxies", ``libc.a`` that holds the NuttX C library,
|
||
and ``libnx.a`` that hold the graphics interface library. (FULLY implemented).
|
||
|
||
Shared Libraries
|
||
^^^^^^^^^^^^^^^^
|
||
|
||
The downside to using static libraries like this is that a function
|
||
in the static libraries would be duplicated in the address environment
|
||
of every process that uses that function. Some very commonly used functions,
|
||
such as ``printf()``, can be quite large and the penalty for duplicating
|
||
``printf()`` in every process address environment could be a significant
|
||
problem.
|
||
|
||
The solution is to used shared libraries, that is, libraries
|
||
of functions that:
|
||
|
||
1. Have only one ``.text`` space in RAM, but
|
||
2. Separate ``.data`` and ``.bass`` space, and are
|
||
3. Separately linked into with the program in each address environmnet.
|
||
|
||
(not implemented).
|
||
|
||
Dynamic Loader
|
||
^^^^^^^^^^^^^^
|
||
|
||
Such shared library support would be a significant yet natural extensions
|
||
to the existing NuttX dynamic loader.(not implemented).
|
||
|
||
|
||
Partially Linked vs Fully Linked Objects
|
||
----------------------------------------
|
||
|
||
Partially Linked Objects
|
||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||
|
||
The NuttX ELF loader currently accepts partially linked objects, that is,
|
||
programs that do not have the final addresses of functions and data resolved.
|
||
|
||
Instead, these ELF program files include relocation and symbol information
|
||
inside them. The NuttX ELF loader then accesses this relocation information
|
||
and resolves all of the function and data addresses when the program
|
||
is loaded into memory. (FULLY implemented).
|
||
|
||
Two things to note about this approach:
|
||
|
||
1. First, note that retaining the relocation information in the ELF program
|
||
files makes the partially linked object files much bigger than necessary
|
||
because they have to carry all of this relocation and symbol information
|
||
along with all of the code and data.
|
||
2. A second thing to note is that every in-memory representation is unique;
|
||
each is a one-off, resolved version of the partially linked object file.
|
||
Each might have been relocated differently.
|
||
A consequence of this is that if there are multiple copies of the same
|
||
program running, all of the code must be duplicated in memory.
|
||
It is not possible to share anything even though the programs may be identical.
|
||
|
||
Fully Linked Objects
|
||
^^^^^^^^^^^^^^^^^^^^
|
||
|
||
These partially linked objects are required in systems that have no MMU.
|
||
In that case, each ELF executable will be loaded into a unique physical memory
|
||
location and, hence, truly will be unique and truly not shareable.
|
||
|
||
But there is a difference if the ELF programs are loaded in the virtualized,
|
||
kernel build; in that case, all ELF executables are loaded into the same
|
||
virtual memory space! The executables can be fully linked at build time
|
||
because the final addresses are known and program files can be stripped
|
||
of all unnecessary relocation and symbolic information.
|
||
|
||
In the fully linked build, all function and data addresses are fully resolved
|
||
to their final virtual addresses when the ELF executable is built.
|
||
|
||
(Possibly functional, but not tested).
|
||
|
||
Shared ``.text`` Regions
|
||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||
|
||
So in the case of fully linked objects, there is no obstacle to sharing
|
||
the code. All that is required is a minor modification to the way that the ELF
|
||
``.text`` region is allocated. If the ELF ``.text`` region is mapped
|
||
into memory from the ELF file using the ``mmap()`` interface instead of
|
||
being allocated from the page memory pool, then the ``.text`` region
|
||
is naturally share-able.
|
||
|
||
NuttX does include a partial implementation of the ``mmap()`` file mapping
|
||
interface but that implementation is tailored for use only in the flat,
|
||
embedded build and cannot be used with the kernel build,
|
||
at least in its current form.
|
||
|
||
That full implementation of ``mmap()`` plus the minor changes
|
||
to the NuttX ELF loader are all that are required to support fully
|
||
share-able ``.text`` sections – as well as the memory savings
|
||
from not carrying aroung the relocation and symbol information
|
||
|
||
(Not implemented).
|
||
|
||
|
||
Memory Management
|
||
-----------------
|
||
|
||
When memory is allocated by the privileged base code or by an unprivileged
|
||
application. The resulting memory allocation must be accessible in and only
|
||
in the address space where the memory was allocated.
|
||
|
||
The strategy of using two heaps as was described above for the simple
|
||
"Protected Build" cannot work in this case.
|
||
Rather, each address space must have its own heap!
|
||
|
||
The function ``malloc()`` must exist in each address environment and memory
|
||
allocated via ``malloc()`` must be available only in that address space.
|
||
|
||
(FULLY implemented, not tested).
|
||
|
||
Page Allocator
|
||
^^^^^^^^^^^^^^
|
||
|
||
In such an environment, memory is used controlled by a simple
|
||
"page allocator". A page allocator is really a very simple memory allocator
|
||
that allocates physical memory in pages that can then be mapped into the
|
||
appropriate address environment using the MMU.
|
||
|
||
The interface between each instance of ``malloc()`` and the base page
|
||
allocator are via the ``brk()`` and ``sbrk()`` system calls.
|
||
|
||
For historic reasons, these system calls deal with something called the break
|
||
value, hence their names, but let's just think of this as how much memory
|
||
is available in the process local heap.(FULLY implemented):
|
||
|
||
.. code-block:: c
|
||
|
||
#include <unistd.h>
|
||
int brk(void *addr);
|
||
void *sbrk(intptr_t incr);
|
||
|
||
* The ``brk()`` function sets the break value to addr and changes the
|
||
allocated space accordingly (not implemented).
|
||
* The ``sbrk()`` function adds incr bytes to the break value and changes
|
||
the allocated space accordingly. If ``incr`` is negative, the amount ofi
|
||
allocated space is decreased by incr bytes.
|
||
The current value of the program break is returned by ``sbrk(0)``.
|
||
(FULLY implemented).
|
||
* See https://www.OpenGroup.org for further information about these system
|
||
calls.
|
||
|
||
Shared Memory
|
||
^^^^^^^^^^^^^
|
||
|
||
Once you have all of the user application logic encapsulated as processes
|
||
within their own private address environment, then you following strict rules
|
||
about how these different user processes communicate with each other.
|
||
Of course, all of the standard Inter-Process Communication (IPC) methods work
|
||
fine: Semaphores, signals, message queues, etc.
|
||
But what about data? How do processes share large, in-memory data sets?
|
||
|
||
From the title of this section you can see that the answer is
|
||
via Shared Memory, that is via chunks of memory which are mapped into each
|
||
process' virtual address space.
|
||
Here are the set of interfaces implemented for this purpose in NuttX:
|
||
|
||
* ``shmget()``. Get the shared memory identifier, the shmid, of a shared
|
||
memory region. The ``shmid`` is like the file descriptor that you get when
|
||
you open a file and ``shmget()`` is much like ``open()``. And like opening
|
||
a file, there are flags that can control where you want to open the shared
|
||
memory object for read/write or read-only purpose or if you want to create
|
||
the shared memory region if it does not exist.
|
||
(FULLY implemented but without protection modes and also untested).
|
||
* ``shmctl()``. Once you have the ``shmid``, you can use that value with other
|
||
interfaces to manage the shared memory interface. ``shmctl()`` will,
|
||
for example, let you get and modify the characteristics of the shared memory
|
||
region. ``shmctl()`` will also let you remove a shared memory region when
|
||
it is no longer needed.
|
||
(FULLY implemented but without privilege modes and also untested).
|
||
* ``shmat()``. Given the shmid, ``shmat()`` can be used to attach the shared
|
||
memory region, i.e., to map it into the user process address space.
|
||
(FULLY implemented but untested)
|
||
* ``shmdt()``. Complementing ``shmat()``, ``shmdt()`` can be used to detachi
|
||
a shared memory region from the user process address space.
|
||
(FULLY implemented but untested)
|
||
|
||
Closely related to to these interfaces are the ``mmap()`` and ``munmap()``
|
||
interfaces. While these interfaces are implemented in NuttX and available
|
||
in the flat build, they have not yet been extended to provide full shared
|
||
memory support as described above for the shm interfaces. (not implemented).
|
||
|
||
|
||
Dual Stacks
|
||
^^^^^^^^^^^
|
||
|
||
Having a program loading from a file system is only interesting if that
|
||
program can also run other programs that reside in a file system.
|
||
But that raises another level of complexity: We cannot instantiate the new
|
||
program environment without also destroying the current program environment.
|
||
So all data must be preserved by copying the caller's data into the common,
|
||
neutral kernel address environment before the switch.
|
||
|
||
But what about the callers stack? That stack lies in the calling process'
|
||
address environment. If in the system call, the kernel logic runs on the
|
||
caller's user stack, then there will almost surely be some disaster down
|
||
the road when switching process contexts.
|
||
How do you avoid losing the caller's stack contents that the C logic needs
|
||
to run while also instantiating the new program's address environment?
|
||
|
||
The usual solution is to have two stacks per thread:
|
||
The large, possibly dynamically sized, user stack and a much smaller kernel
|
||
stack. When the caller issues the system call, the system call logic switches
|
||
stacks: It replaces the stack pointer with the reference to the user stack
|
||
with a new reference to the thread's kernel stack. Then the system
|
||
all executes with a process neutral kernel stack avoiding the stack transition
|
||
problems. The system call logic then restores the program's stack pointer
|
||
before returning from the system call (this is, of course, complicate
|
||
by the possibility of nested system calls and system calls that can generate
|
||
context switches or can dispatch signals).
|
||
|
||
Doesn't it cost a lot of memory to have two stacks?
|
||
Yes and no, depending on what kind of memory usage you are used to.
|
||
Remember that with the MMU, the user memory space is quantized into units
|
||
of pages which are typically 4KiB and can grow upward from there in multiples
|
||
of 4KiB as needed. The kernel stack has limited depth.
|
||
It does not need to be dynamically sized and can probably be very small
|
||
(perhaps as little as 1KiB). So, yes, the dual stacks do use more memory
|
||
but the impact is not as significant as might first think.
|
||
|
||
(Dual stack support is FULLY implemented in NuttX).
|
||
|
||
|
||
Further Down the Road
|
||
---------------------
|
||
|
||
Other Topics
|
||
^^^^^^^^^^^^
|
||
|
||
If all of the above were implemented, then NuttX could probably rightfully
|
||
claim to be a small Unix work-alike. From there, several additional topics
|
||
could be addresses but this is too far down the roadmap for me
|
||
the contemplate in any real detail:
|
||
|
||
* On-Demand Paging and Swap. Why not keep programs and data in a file system
|
||
and swap the state into physical memory as needed?(not implemented).
|
||
* ``mmap()``. True shared memory and true file mapping could be supported.
|
||
I am repeating myself (not implemented).
|
||
* ``fork()``. The ``fork()`` interface could be supported. NuttX currently
|
||
supports the "crippled" version, ``vfork()`` but with these process address
|
||
environments, the real ``fork()`` interface could be supported.
|
||
(not implemented).
|
||
* Dynamic Stack Allocation. Completely eliminate the need for constant tuning
|
||
of static stack sizes.(not implemented).
|
||
* Shared Libraries. Am I repeating myself again?(not implemented).
|
||
* Program build tools. Then how do we make building programs for NuttX easy.
|
||
It should be as easy as arm-nuttx-eabi-gcc myprogram.c.(not implemented).
|
||
|
||
|
||
Some Conseuences
|
||
----------------
|
||
|
||
task_create()
|
||
^^^^^^^^^^^^^
|
||
|
||
In the traditional, flat NuttX build, the interface ``task_create()`` and
|
||
``task_spawn()`` are the standard interfaces for creating new tasks.
|
||
Both take the entry point address of the new task as an argument.
|
||
These task creation interface, however, cannot be used within code
|
||
executing in an address environment.
|
||
|
||
Why not? It is kind of a long story.
|
||
|
||
Remember that when a new task is created, a new task group is also created.
|
||
That task group provides all of the resources shared between the parent task
|
||
and all of its child threads.
|
||
One of these resources that is shared is the address environment.
|
||
|
||
The behavior of ``task_create()`` is to create a new task with no address
|
||
environment, or more correctly with the common kernel address environment.
|
||
|
||
When the parent process was created, it was created in the its own private
|
||
address. The address of the new task entry point passed to ``task_create()``
|
||
must lie in this same private address environment.
|
||
|
||
So you can see that we have a perverse configuration here:
|
||
The ``.text`` address of the new task lies in the private address space
|
||
of the parent task; but the task itself uses the common OS address space.
|
||
There are two problems here:
|
||
|
||
* (1) because of privilege issues, the new task may not have access either
|
||
to the parent task's address environment or to the common kernel address
|
||
environment. It may just crash immediately, depending upon how these things
|
||
are configured.
|
||
* Or assuming that there are no privilege issues, (2) the new task certainly
|
||
will depend on the address space of the parent task being in place in order
|
||
to run. As soon as the parent's address space disappears
|
||
(due, perhaps, because the parent task's address space was de-activated
|
||
or perhaps the parent task exits destroying its address environment).
|
||
The child task would crash immediately thereafter.
|
||
|
||
So how do you create new tasks/processes in such a context.
|
||
There is only one way possible; by using an interface that takes a file name
|
||
as an argument (rather than absolute address).
|
||
New processes started with ``vfork()`` and ``exec()`` or with
|
||
``posix_spawn()`` should not have any of these issues.
|
||
|
||
|
||
ARM Memory Management
|
||
---------------------
|
||
|
||
ARM Page Table Organization
|
||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||
|
||
Cortex-A Page Tables. Let's focus on the ARMv7-A (aka, Cortex-A) MMU for now.
|
||
That MMU uses a two-level page table:
|
||
|
||
* Level 1 Page Table
|
||
|
||
* The size of virtually addressable region is 4GiB.
|
||
* The size of first level page table is 4096 entries (16KiB).
|
||
* Each each entry in the page table provides the mapping for 1MiB
|
||
of virtual memory. One entry may be either:
|
||
* A section mapping which maps 1MiB of contiguous virtual addresses to 1MiB
|
||
of physical memory (only occasionally used), or
|
||
* It may refer to a beginning of a second level page table
|
||
(much more commonly used).
|
||
|
||
* Level 2 Page Table
|
||
|
||
* Differing pages sizes are possible, but use of 4KiB pages used
|
||
(this is the smallest page size for the Cortex-A).
|
||
* With a 4KiB page table, 256 page table entries (PTEs) are required
|
||
to span the 1Mib region.
|
||
|
||
Advantages / Disavantages
|
||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||
|
||
The most important benefits of the ARMv7-A page table are:
|
||
|
||
* The TLB reloads are done automatically by the hardware without software
|
||
intervention. This is a huge performance advantage to other architecture
|
||
where each mapping must be instantiated via logic in a page fault exception
|
||
handler.
|
||
* The multiple levels and configurable page sizes add flexibility.
|
||
|
||
|
||
The ARMV7-A page table is, in fact, well-suited for higher end platforms that
|
||
do not suffer from memory and performance constraints.
|
||
But for the most constrained platforms, the following is a big issue:
|
||
|
||
* Each page table required 16KiB of memory PLUS 1KiB of memory for each
|
||
level 2 page table (assuming 4KiB page size).
|
||
That would result in a maximum size of over 4MiB!
|
||
* The state of the page table is part of the process' context and must be
|
||
saved and restored on every context switch!
|
||
|
||
Let's look first at how Linux deals with the ARM page tables;
|
||
then let's propose a scaled down approach for NuttX.
|
||
|
||
ARM/Linux Page Table Notes
|
||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||
|
||
Linux Summary
|
||
~~~~~~~~~~~~~
|
||
|
||
This is a summary of how the Linux kernel uses the page table to control
|
||
a process' memory mapping with a Cortex-A CPU.
|
||
This is not based upon my authoritative knowledge, but is rather based
|
||
on Google for explanations.
|
||
|
||
Virtual Address Space Partitioning
|
||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||
|
||
* The 4GiB virtual address is partitioned with 3GiB of user space and 1GiB
|
||
of kernel space: Virtual address ``0x0000:0000-0xbfff:ffff`` is user space
|
||
while ``0xc000:0000-0xffff:ffff`` is kernel space.
|
||
* Level 1 page table entries ``0-3071`` map user space virtual addressees
|
||
and entries ``3072-4095`` map the kernel space addresses.
|
||
|
||
Process Page Tables
|
||
~~~~~~~~~~~~~~~~~~~
|
||
|
||
* The ARM co-processor register ``TTBR0`` holds the address for the current
|
||
page directory (the page table that the MMU is using for translations).
|
||
* Each user process has its own page table located in the kernel
|
||
address space.
|
||
* For each process context switch, the kernel changes the ``TTBR0`` to the
|
||
new user process page table.
|
||
* Only ``TTBR0`` is used. ``TTBR1`` only holds the address of the initial
|
||
swapper page (which contains all the kernel mappings) and isn't really used
|
||
for virtual address translations.
|
||
* For each new user process, the kernel creates a new page table, copies all
|
||
the kernel mappings from the swapper page (page frames from 3-4GiB) to the
|
||
new page table and clears the user pages (page frames from 0-3GiB).
|
||
It then sets ``TTB0`` to the base address of this page directory and flushes
|
||
cache to install the new address space.
|
||
The swapper page is also always kept up to date with changes to the mappings.
|
||
* The swapper page is usually located at addresses ``0xc0004000-0xc0008000``.
|
||
|
||
ARM/NuttX Page Table Proposal
|
||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||
|
||
Size Reduction Tradeoff
|
||
~~~~~~~~~~~~~~~~~~~~~~~
|
||
|
||
We can reduce the memory usage by page tables in NuttX by:
|
||
|
||
* Keeping only a single level 1 page table that is shared by all task groups.
|
||
* Level 2 page tables are, of course, still need to be duplicated for each
|
||
process.
|
||
* To avoid copying the 3GiB mapping used by Linux, we can simply reduce
|
||
the size of the virtual address space so that instead of copying
|
||
3,072 entries on each process switch, we copy perhaps 4.
|
||
That would limit the virtual address range for each process from 3GiB
|
||
to only 4MiB. But that is probably reasonable but would also be configurable.
|
||
* Reducing the supported virtual address from 3GiB to, say, 4MiB would also
|
||
reduce the amount of memory that has to be allocated for each process.
|
||
Continuing with this 4MiB suggestion, it would following that no more than
|
||
4KiB would need to set aside for level 2 page table support for each process.
|
||
* Further, let's not make any assumptions about what virtual address range
|
||
corresponds to user space and which corresponds to kernel space.
|
||
It is simply an agreement that must be made between the platform
|
||
implementation and the program's linker script.
|
||
|
||
Per-Process/Per-Thread Regions
|
||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||
|
||
Three regions must set aside for each process to hold:
|
||
|
||
* A level 2 mapping for the ``.text`` region,
|
||
* A level 2 mapping for the static data region (``.bss`` and ``.data``), and
|
||
* A level 2 mapping for the process' heap.
|
||
|
||
Instantiation/Extensibility
|
||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||
|
||
The first two mappings would be created when the program is loaded
|
||
into memory; The first heap mapping would be created when ``sbrk()``
|
||
is first called. All three could be extended at runtime if shared libraries
|
||
are supported (augmenting ``.text``, ``.bss`` and ``.data``) or when the heap
|
||
is extended by subsequent ``sbrk()`` calls.
|
||
|
||
Configuration
|
||
^^^^^^^^^^^^^
|
||
|
||
The following configuration options are proposed:
|
||
|
||
* ``CONFIG_ARCH_TEXT_VBASE`` - The virtual address of the beginning
|
||
the ``.text`` region.
|
||
* ``CONFIG_ARCH_DATA_VBASE`` - The virtual address of the beginning
|
||
of the ``.bss``/``.data`` region.
|
||
* ``CONFIG_ARCH_HEAP_VBASE`` - The virtual address of the beginning
|
||
of the heap region.
|
||
* ``CONFIG_ARCH_TEXT_NPAGES`` - The maximum number of pages that can be
|
||
allocated for the ``.text`` region. This, along with knowledge of the page
|
||
size, determines the size of the ``.text`` virtual address space.
|
||
Default is 1.
|
||
* ``CONFIG_ARCH_DATA_NPAGES`` - The maximum number of pages that can be
|
||
allocated for the ``.bss``/``.data`` region. This, along with knowledge
|
||
of the page size, determines the size of the ``.bss``/``.data`` virtual
|
||
address space. Default is 1.
|
||
* ``CONFIG_ARCH_HEAP_NPAGES`` - The maximum number of pages that can be
|
||
allocated for the heap region. This, along with knowledge of the page size,
|
||
determines the size of the heap virtual address space. Default is 1.
|
||
|
||
Implementation
|
||
^^^^^^^^^^^^^^
|
||
|
||
The task group resources are retained in a single structure,
|
||
``task_group_s`` that is defined in the header file
|
||
``nuttx/include/nuttx/sched.h``.
|
||
The type ``group_addrenv_t`` must be defined by platform specific logic
|
||
in ``nuttx/arch/*/include/arch.h``.
|
||
This is a first cut proposal at that type might be:
|
||
|
||
.. code-block:: c
|
||
|
||
struct group_addrenv_s
|
||
{
|
||
FAR uintptr_t *text[ARCH_TEXT_NSECTS];
|
||
FAR uintptr_t *data[ARCH_DATA_NSECTS];
|
||
FAR uintptr_t *heap[ARCH_HEAP_NSECTS];
|
||
};
|
||
typedef struct group_addrenv_s group_addrenv_t
|
||
|
||
Where each 1MiB section refers to a level 2 page table that maps
|
||
256 4KiB pages:
|
||
|
||
.. code-block:: c
|
||
|
||
#define __PG2SECT_SHIFT (20 - MM_PGSHIFT)
|
||
#define __PG2SECT_MASK ((1 << __PG2SECT_SHIFT) - 1)
|
||
#define ARCH_PG2SECT(p) (((p) + __PG2SECT_MASK) >> __PG2SECT_SHIFT)
|
||
#define ARCH_SECT2PG(s) ((s) << __PG2SECT_SHIFT)
|
||
#define ARCH_TEXT_NSECTS ARCH_PG2SECT(CONFIG_ARCH_TEXT_NPAGES)
|
||
#define ARCH_DATA_NSECTS ARCH_PG2SECT(CONFIG_ARCH_DATA_NPAGES)
|
||
#define ARCH_HEAP_NSECTS ARCH_PG2SECT(CONFIG_ARCH_HEAP_NPAGES)
|
||
|
||
These tables would hold the physical address of the level 2 page tables.
|
||
All would be initially ``NULL`` and would not be backed up with physical
|
||
memory until mappings in the level 2 page table are required.
|
||
|
||
Per-Thread Regions
|
||
^^^^^^^^^^^^^^^^^^
|
||
|
||
One region must set aside for each thread to hold:
|
||
|
||
* The thread's stack
|
||
|
||
This stack would be initially of size zero and would be backed-up with
|
||
physical pages during page fault exception handling to support dynamically
|
||
sized stacks for each thread.
|
||
|
||
The following configuration options are proposed:
|
||
|
||
* ``CONFIG_ARCH_STACK_VBASE`` - The virtual address of the beginning
|
||
of the stack region
|
||
* ``CONFIG_ARCH_STACK_NPAGES`` - The maximum number of pages that can be
|
||
allocated for the stack region. This, along with knowledge of the page size,
|
||
determines the size of the stack virtual address space. Default is 1.
|
||
|
||
The thread resources are retained in a single structure, ``tcb_s`` that is
|
||
defined in the header ``file nuttx/include/nuttx/sched.h``.
|
||
The type ``xcptcontext`` must be defined by platform specific logic
|
||
in ``nuttx/arch/*/include/irq.h``.
|
||
This structure might be extended to include:
|
||
|
||
.. code-block:: c
|
||
|
||
FAR uintptr_t *stack[ARCH_STACK_NSECTS];
|
||
|
||
Where again:
|
||
|
||
.. code-block:: c
|
||
|
||
#define ARCH_STACK_NSECTS ARCH_PG2SECT(CONFIG_ARCH_STACK_NPAGES)
|
||
|
||
Context Switches
|
||
^^^^^^^^^^^^^^^^
|
||
|
||
Then what happens on each context switch?
|
||
|
||
* Since there is only a single page table, ``TTBR0`` never changes.
|
||
* Instead, the particular Level 1 page entries are replace based upon the
|
||
physical page allocations in ``group_addrenv_t`` and ``xcptcontext``.
|
||
* Assuming again a 4MiB per-process virtual address space, at most only
|
||
four elements of the level 1 page table would have to change:
|
||
``.text``, ``.bss``/``.data``, heap, and stack.
|
||
* For context switches within the same task group, only the stack level 1
|
||
table entry would need to change.
|
||
* The MMU TLBs and processor caches would still have to be flushed
|
||
and invalidated for the (smaller) user virtual address range.
|
||
|
||
|
||
Terminology
|
||
===========
|
||
|
||
* **Address Environment:** This is really a generic phrase to refers to the
|
||
memory addressable by the software. However, in the context of this
|
||
document, we will be referring to something more specific.
|
||
We will be referring a memory architecture that supports multiple.
|
||
per-task address environments: Each task can execute within its own
|
||
address environment. Tasks with private address environments
|
||
may sometimes be called processes.
|
||
* **Blob:** A block of code and/or data within a restricted range
|
||
of contiguous addresses.
|
||
* **Call Gate:** A call gate is a mechanism for calling into privileged code
|
||
from unprivileged code, raising the privilege level of the thread
|
||
temporarily for the call. A typical method of implementing a call gate
|
||
is through software trap or software interrupt instructions:
|
||
The interrupt will place the thread into a privileged mode of operation
|
||
where it can then execute the call.
|
||
These call gates are used to implement system calls (``SYSCALLS``),
|
||
i.e., calls from user application code into OS system services.
|
||
* **Flat Address Space:** An address space is flat if either
|
||
(1) there is no mapping of physical addresses to virtual addresses, or
|
||
(2) there is a 1-to-1 mapping of the physical address space
|
||
to a virtual address space.
|
||
* **Identity Mapping:** When a CPU has a MMU that is used to map the physical
|
||
address space to a virtual address space and the virtual addresses map
|
||
to the same virtual address. In this case, the MMU is is not being used
|
||
for memory mapping, but rather only for its ability to color memory regions.
|
||
* **MMU:** Memory Management Unit. Can be configured to map physical addresses
|
||
in one address region to virtual addresses in a different address region.
|
||
Can also color an address region by controlling the privileges required
|
||
to access the memory, the cache properties of the memory,
|
||
and other memory attributes.
|
||
* **MPU:** Memory Protection Unit. Can be be configured to protect memory.
|
||
* **Page:** The size of a block of memory that can be mapped using an MMU.
|
||
The MMU can handle pages of different sizes. Other terminology may be used
|
||
for very large pages; ARM calls these sections. And sections may be divided
|
||
down to to smaller pages of differing sizes.
|
||
* **Page Fault:** When the MMU is unable to map a virtual address to a physical
|
||
address, then a page fault occurs. The page fault means that there is no TLB
|
||
in the MMU that can provided the necessary mapping.
|
||
In response to a page fault, the MMU may consult a page table in an attempt
|
||
to resolve the page fault or it may generate a page fault interrupt so that
|
||
software can resolve the page fault.
|
||
An unresolvable page fault is fatal and usually results in a crash.
|
||
* **Page Table:** A data structure in memory that is accessed by the MMU to
|
||
reload TLBs. The able has a precise format determined by the MMU hardware.
|
||
It is configured by software to support the desired mapping and accessed
|
||
via hardware DMA when a page fault occurs.
|
||
* **Physical Address:** The actually address that appears on the CPU bus and
|
||
provided to the memory parts when the memory is accessed.
|
||
* **PTE:** Page Table Entry.
|
||
* **TLB:** Translation Look-Aside Buffer. An element of the MMU that maps
|
||
one page of memory. This may be loaded by explicit logic as part of page
|
||
fault handling (as is typically done with MIPS) or may be loaded
|
||
automatically by the MMU from a page table (as with the ARM).
|
||
* **Virtual Address:** The memory addresses used by the software running
|
||
in the CPU. This might be mapped to a different virtual address by an MMU.
|
||
|