Hi Paul,
On Sun Apr 10, 2022 at 18:58:10 +0200, Paul Boddie wrote:
I finally got round to experimenting with L4Re again, but in attempting to investigate task creation, I seem to have some difficulties understanding the mechanism by which tasks are typically created and how the l4_task_map function might be used in the process.
After looking at lots of different files in the L4Re distribution, my understanding of the basic mechanism is as follows:
- Some memory is reserved for the UTCB of a new task, perhaps using the
l4re_ma_alloc_align function (or equivalent) to obtain a dataspace.
No, for UTCBs there's a dedicated call l4_task_add_ku_mem in case one needs more UTCB memory than has been initially created with l4_factory_create_task().
- A task is created using l4_factory_create_task, indicating the UTCB
flexpage, with this being defined as...
l4_factory_create_task(l4re_env()->factory, new_task, l4_fpage(utcb_start, utcb_log2size, L4_FPAGE_RW))
Yes. Here the flexpage defines where memory usable for UTCBs shall be created.
- A thread is created using l4_factory_create_thread.
l4_factory_create_thread(l4re_env()->factory, new_thread)
The thread attributes are set using the l4_thread_control API.
The l4_thread_ex_regs function is used to set the instruction pointer
(program counter) and stack pointer of the thread.
- The l4_scheduler_run_thread function is used to initiate the thread.
All yes.
The expectation is that the thread will immediately fault because there is no memory mapped at the instruction pointer location. However, it seems to me that it should be possible to use l4_task_map to make a memory region available within the task's address space, although I don't ever see this function used in L4Re for anything.
(The C++ API makes it difficult to perform ad-hoc searches for such low-level primitives, in my view, so perhaps I am missing use of the equivalent methods.)
Indeed. L4::Task::map is used, for example to map some initial capabilities, and typically not memory.
Tentatively, I would imagine that something like this might work:
l4_task_map(new_task, L4RE_THIS_TASK_CAP, l4_fpage(program_start, program_log2size, L4_FPAGE_RX), task_program_start)
Here, the program payload would be loaded into the creating task at program_start, but the new task would be receiving the payload at task_program_start, with the configured instruction pointer location occurring within the receive window (after task_program_start, in other words).
Yes, this would work.
There are, of course, many other considerations around creating tasks, which I have noted from looking at the different packages (libloader, l4re_kernel, moe, ned), and I am aware that a few other things need to be done to start a task such as...
- Defining capability selectors and mapping appropriate capabilities to the
new task.
- Creating a stack for the task and populating it with arguments and
environment information.
- Defining a suitable pager and exception handler, with this usually being
provided by the l4re binary, as I understand it.
Yes, this all needs to be done.
Also, when actually dealing with program loading generally, I realise that the ELF binary needs to be interpreted and the appropriate regions associated with different parts of memory, this typically being handled by the region mapper/ manager in L4Re. And there is also the matter of dynamic library loading.
Yes, indeed.
But here, I am just attempting to establish the basic mechanism when a task starts up. Unfortunately, the only discussion I found was this (after some initial discussion about a related topic):
http://os.inf.tu-dresden.de/pipermail/l4-hackers/2014/015366.html
There are various examples in Subversion (maybe somewhere in the Git repositories, too) that create tasks or threads, but I don't find them particularly helpful, apparently being oriented towards very specific applications. A previous example was referenced in the above thread for the older L4Env system (or maybe an even earlier system):
http://os.inf.tu-dresden.de/pipermail/l4-hackers/2000/000384.html
As for why I would be wondering about such things - a question inevitably asked in the first thread referenced above - I firstly want to be able to understand the mechanism involved, but I also want to be able to integrate work I have been doing on file paging into task creation.
I think you described it very well with your steps listed above. Also, l4util has a l4util_create_thread() function that lists all the steps needed to create a thread in an existing task.
Although I can probably do this by customising the "app model" normally used by the different loaders, it seems that I would need to construct an alternative l4re binary, which is rather cumbersome and perhaps a weakness of the abstractions that are provided, these being rather oriented towards obtaining dataspaces via the namespace API which I don't want to have to support in my filesystem.
In case you want to use other abstractions you probably need to adapt l4re to use them such that they fit together.
In any case, I wonder if there are any resources that describe the use of l4_task_map and the details of the program environment within tasks.
l4_task_map() has documentation: https://l4re.org/doc/group__l4__task__api.html#ga8ed2ff7ba204de7c01311c22412... and is a direct API to the kernel for mapping resources, defined by l4sys. At this level, there is not really a definition of how a program environment looks like. However, as Fiasco needs to supply its initial programs some capabilities, those are defined (https://l4re.org/doc/group__l4__cap__api.html#gaa7801b63edba351bad9ea8026432...). What moe and ned do, is similar, but not necessarily the same, as they provide a more powerful interface to this (https://l4re.org/doc/group__api__l4re__env.html) and also provide all the functionality normal programs enjoy, like argument lists, environment variables, etc.
Adam