mirror of
https://github.com/apache/nuttx.git
synced 2026-10-08 14:55:18 +00:00
The documentation grew one page at a time, so the tree follows the
history of who wrote what and not the shape of NuttX. Scheduling is
spread over three places, a driver page can sit above the subsystem
that owns it, and the front page lists everything at the same level.
That is a lot to face when all you want to know is where the scheduler
lives.
This change files every page under the code it describes. It is a move,
not a rewrite: outside the ten pages named below, every page keeps the
text that is already in master, and no page's text is deleted.
What it does:
* Groups the table of contents into nine chapters.
* Moves the OS subsystems under os/: scheduling, memory, drivers,
filesystem, networking, IPC, interrupts, libs, time.
* Renames the platform pages to the names the source tree uses, and
derives their tags from the tree instead of by hand.
* Splits guides/ by subject.
* Adds Documentation/redirects.py, with a rule for every page that left
its old path, so old URLs keep working. The redirect page also carries
a link's #anchor across to the new page.
Ten pages have text that is new or rewritten. Nine of them are the
landing page of a chapter, which has to exist for the new structure:
index the front page
os/index OS Design
os/scheduling/index Scheduling
os/interrupts/index Interrupts
os/ipc/index IPC
os/time/index Time and timers
about/index About
developing/index Developing NuttX
ReleaseNotes/index Release notes
The tenth is os/libs/libbuiltin, the only page here with technical
content: libs/libbuiltin/ had no page at all. Five SVG diagrams come
with these pages, hand-written XML with no editor metadata.
Nothing outside Documentation/ is touched.
How it was checked:
* Sphinx builds with -W: no warnings, and no document left outside a
toctree.
* A script, offered in the PR, proves the narrow claim this rests on.
For every page outside the ten named above it erases what a move
touches -- link target, path, tag line, toctree block, table border --
from the whole old text and the whole new text, and requires the two
to be byte for byte identical. It also requires every sentence of a
deleted page to turn up somewhere, and every page that left its old
path to have a redirect, from a URL that existed, to where its content
went. It exits non-zero and names the page if any of that is not true,
and it tests added pages too, so forgetting to declare one cannot make
it pass.
* An independent audit checked 133 factual claims on these ten pages
against the tree, one shell command per claim: 130 confirmed, 1
refuted and fixed here, 2 not checkable.
* tools/checkpatch.sh is clean over the range.
The diff is large because moving a page changes every link that points
to it. Most of it is pure renames, and board pages that gained one tag
line.
Assisted-by: Claude:claude-opus-5
799 lines
34 KiB
ReStructuredText
799 lines
34 KiB
ReStructuredText
.. _memory-configurations:
|
||
|
||
=====================
|
||
Memory Configurations
|
||
=====================
|
||
|
||
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 environment.
|
||
|
||
(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 around 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 real ``fork()`` interface can be supported on configurations
|
||
with a duplicable process address environment: an architecture implements
|
||
``up_addrenv_fork()`` and selects ``CONFIG_ARCH_HAVE_FORK``. What is not
|
||
implemented is
|
||
copy-on-write: the duplication copies the parent's pages eagerly, which
|
||
needs as much free memory as the parent occupies. Demand paging would fix
|
||
that.
|
||
* 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 ``fork()`` or ``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.
|
||
|