1、outcome
- 了解行话 TCB。
- 了解如何在同一地址空间中启动线程。
- 了解如何读取和更新 TCB 寄存器状态。
- 了解如何挂起和恢复线程。
- 了解线程优先级及其与 seL4 调度程序的交互。
- 获得对异常和调试故障处理程序的基本了解。
2、background
Thread Control Blocks
seL4 提供线程来表示执行上下文和管理处理器时间。 seL4 中的线程由线程控制块对象 (TCB) 实现,每个内核线程都有一个。
TCB 包含以下信息:
a priority and maximum control priority,
register state and floating-point context,
CSpace capability,
VSpace capability,
endpoint capability to send fault messages to,
and the reply capability slot.
优先级和最大控制优先级,
寄存器状态和浮点上下文,
CSpace能力,
VSpace能力,
向其发送故障消息的端点能力,
和回复能力槽。
Scheduling model
seL4 调度器选择下一个线程在特定的处理内核上运行,是一个基于优先级的循环调度器。 调度程序选择可运行的线程:即,已恢复且未在任何 IPC 操作上阻塞的线程。
优先级
调度程序选择优先级最高的可运行线程。 seL4 提供的优先级范围为 0-255,其中 255 是最大优先级(在 libsel4
中编码为 seL4_MinPrio
和 seL4_MaxPrio
)。
TCB 还具有最大控制优先级 (MCP),它作为一种非正式的优先级能力。 设置 TCB 的优先级时,必须提供明确的 TCB 能力以获取设置优先级的权限。 正在设置的优先级与授权 TCB 的 MCP 进行检查,目标优先级更高,操作失败。 根任务开始时优先级和 MCP 都设置为 seL4_MaxPrio
。
轮循
当多个 TCB 可运行且具有相同优先级时,它们会以先进先出的循环方式进行调度。 更详细地说,内核时间以称为节拍的固定时间量计算,每个 TCB 都有一个时间片字段,表示 TCB 在被抢占之前有资格执行的节拍数。 内核计时器驱动程序配置为触发标记每个滴答的周期性中断,并且当时间片用完时应用轮询调度。 线程可以使用 seL4_Yield 系统调用放弃它们的当前时间片。
域调度
为了提供机密性,seL4 提供了一个顶级分层调度器,它提供静态的、循环调度的调度分区,称为域。 域是在编译时使用循环调度静态配置的,并且是不可抢占的,从而导致域的完全确定性调度。
线程可以分配给域,并且线程仅在其域处于活动状态时才被调度。 跨域 IPC 延迟到域切换,域之间的 seL4_Yield
是不可能的。 如果在调度域时没有线程可运行,则特定于域的空闲线程将运行,直到发生切换。
将线程分配给域需要访问 seL4_DomainSet
能力。 这允许一个线程被添加到任何域。
/* Set thread's domain */
seL4_Error seL4_DomainSet_Set(seL4_DomainSet _service, seL4_Uint8 domain, seL4_TCB thread);
类型 | 名字 | 描述 |
---|---|---|
seL4_DomainSet | _service | 引用的调度域控制能力句柄。自当前线程根CNode按机器字位数解析,下同 |
seL4_Uint8 | domain | 新的调度域 |
seL4_TCB | thread | 要配置的TCB能力句柄 |
返回值: 返回 0 表示成功,非 0 值表示有错误发生。
3、Exercises
本教程将指导您使用 TCB 调用在同一地址空间中创建新线程并将参数传递给新线程。 此外,您还将了解如何调试虚拟内存故障。
在本教程结束时,您想要生成一个新线程来调用下面代码示例中的函数。
int new_thread(void *arg1, void *arg2, void *arg3) {
printf("Hello2: arg1 %p, arg2 %p, arg3 %p\n", arg1, arg2, arg3);
void (*func)(int) = arg1;
func(*(int *)arg2);
while(1);
}
CapDL 加载程序
之前的教程是在根任务中进行的,其中起始 CSpace 布局由 seL4 引导协议设置。 本教程使用 capDL 加载器,这是一个分配静态配置对象和功能的根任务。
capDL 加载器解析系统的静态描述和相关的 ELF 二进制文件。 它主要用于 Camkes 项目,但我们也在教程中使用它来减少冗余代码。 您构建的程序将以其自己的 CSpace 和 VSpace 结束,它们与根任务是分开的,这意味着像 seL4_CapInitThreadVSpace
这样的 CSlot
在 capDL 加载程序加载的应用程序中没有意义。
配置 TCB
当您第一次构建并运行本教程时,您应该会看到如下内容:
释放所有 tcb! 下表由名为 seL4_DebugDumpScheduler()
的调试系统调用生成。 seL4 有一系列在调试内核构建中可用的调试系统调用。 可以在libsel4
中找到可用的调试系统调用。
seL4_DebugDumpScheduler()
用于转储调度程序的当前状态,可用于调试系统似乎已挂起的情况。
在 TCB 表之后,您可以看到 seL4_Untyped_Retype
调用因参数无效而失败。 加载程序已配置为设置以下功能和符号:
//当前线程的根CNode
extern seL4_CPtr root_cnode;
// 当前线程的VSpace
extern seL4_CPtr root_vspace;
// 当前线程的TCB
extern seL4_CPtr root_tcb;
// Untyped object large enough to create a new TCB object
extern seL4_CPtr tcb_untyped;
extern seL4_CPtr buf2_frame_cap;
extern const char buf2_frame[4096];
// 用于新 TCB 对象的空槽
extern seL4_CPtr tcb_cap_slot;
// VSpace中IPC缓冲区映射的符号,以及映射的能力
extern seL4_CPtr tcb_ipc_frame;
extern const char thread_ipc_buff_sym[4096];
// 16 * 4KiB 堆栈映射顶部的符号和映射的能力
extern const char tcb_stack_base[65536];
static const uintptr_t tcb_stack_top = (const uintptr_t)&tcb_stack_base + sizeof(tcb_stack_base);
练习
使用上面提供的功能修复 seL4_Untyped_Retype
调用(如下所示),以便在 tcb_cap_slot
中创建一个新的 TCB 对象。
int main(int c, char* arbv[]) {
printf("Hello, World!\n");
seL4_DebugDumpScheduler();
// TODO fix the parameters in this invocation
seL4_Error result = seL4_Untyped_Retype(seL4_CapNull, seL4_TCBObject, seL4_TCBBits, seL4_CapNull, 0, 0, seL4_CapNull, 1);
ZF_LOGF_IF(result, "Failed to retype thread: %d", result);
seL4_DebugDumpScheduler();
返回0表示成功,非0错误。
创建 TCB 后,它将作为“tcb_threads”
的子项显示在 seL4_DebugDumpScheduler()
输出中。 在整个教程中,您可以使用此系统调用来调试您设置的某些 TCB 属性。
在调度表之后,您应该看到另一个错误:
<<seL4(CPU 0) [decodeInvocation/530 T0xffffff800813fc00 "tcb_threads" @4004bf]: Attempted to invoke a null cap #0.>>
main@threads.c:46 [Cond failed: result]
Failed to configure thread: 2
练习
现在您有了一个 TCB 对象,将其配置为与当前线程具有相同的 CSpace
和 VSpace
。 使用我们提供的 IPC 缓冲区,但不要设置故障处理程序,因为内核会打印我们在调试构建中收到的任何故障。
//TODO fix the parameters in this invocation
result = seL4_TCB_Configure(seL4_CapNull, seL4_CapNull, 0, seL4_CapNull, 0, 0, (seL4_Word) NULL, seL4_CapNull);
ZF_LOGF_IF(result, "Failed to configure thread: %d", result);
您现在应该收到以下错误:
<<seL4(CPU 0) [decodeSetPriority/1035 T0xffffff8008140c00 "tcb_threads" @4012ef]: Set priority: author>
main@threads.c:51 [Cond failed: result]
Failed to set the priority for the new TCB object.
Change priority via seL4_TCB_SetPriority
新创建的线程的优先级为 0,而加载程序创建的线程的优先级为 254。您需要更改新线程的优先级,以便它与当前线程轮流调度。
练习
使用 seL4_TCB_SetPriority
设置优先级。 请记住,要设置线程的优先级,调用线程必须具有这样做的权限。 在这种情况下,主线程可以使用自己的 TCB 能力,其 MCP 为 254。
// TODO 使用当前线程的权限修复设置优先级的调用
// and change the priority to 254
result = seL4_TCB_SetPriority(tcb_cap_slot, seL4_CapNull, 0);
ZF_LOGF_IF(result, "Failed to set the priority for the new TCB object.\n");
seL4_DebugDumpScheduler();
修复 seL4_TCB_SetPriority
调用应该可以让您看到线程的优先级现在设置为与下一个 seL4_DebugDumpScheduler()
调用中的主线程相同。
Name State IP Prio Core
--------------------------------------------------------------------------------------
child of: 'tcb_threads' inactive (nil) 254 0
tcb_threads running 0x4012ef 254 0
idle_thread idle (nil) 0 0
rootserver inactive 0x4024c2 255 0
<<seL4(CPU 0) [decodeInvocation/530 T0xffffff8008140c00 "tcb_threads" @4012ef]: Attempted to invoke a >
main@threads.c:57 [Err seL4_InvalidCapability]:
Failed to write the new thread's register set.
设置初始寄存器状态
TCB 几乎准备好运行,除了它的初始寄存器。 您需要将程序计数器和堆栈指针设置为有效值,否则您的线程将立即崩溃。
libsel4utils
包含一些以平台不可知的方式设置寄存器内容的函数。 您可以使用这些方法来设置程序计数器(指令指针)和堆栈指针。 注意:假设堆栈在所有平台上向下增长。
练习
设置新线程去 调用函数new_thread
。 您可以使用调试系统调用来验证您是否至少正确设置了指令指针 (IP)。
seL4_UserContext regs = {0};
int error = seL4_TCB_ReadRegisters(tcb_cap_slot, 0, 0, sizeof(regs)/sizeof(seL4_Word), ®s);
ZF_LOGF_IFERR(error, "Failed to read the new thread's register set.\n");
// TODO 使用有效的指令指针
sel4utils_set_instruction_pointer(®s, (seL4_Word)NULL);
// TODO 使用有效的堆栈指针
sel4utils_set_stack_pointer(®s, NULL);
// TODO 修复此调用的参数
error = seL4_TCB_WriteRegisters(seL4_CapNull, 0, 0, 0, ®s);
ZF_LOGF_IFERR(error, "Failed to write the new thread's register set.\n"
"\tDid you write the correct number of registers? See arg4.\n");
seL4_DebugDumpScheduler();
成功后,您将看到以下输出:
<<seL4(CPU 0) [decodeInvocation/530 T0xffffff800813fc00 "tcb_threads" @4004bf]: Attempted to invoke a null cap #0.>>
main@threads.c:63 [Err seL4_InvalidCapability]:
Failed to start new thread.
Start the thread
最后,您准备启动线程,这使 TCB 可运行并有资格被 seL4 调度程序选择。 这可以通过将 seL4_TCB_WriteRegisters
的第二个参数更改为 1 并删除 seL4_TCB_Resume
调用,或通过修复下面的 resume 调用来完成。
练习恢复新线程。
// TODO 恢复新线程
error = seL4_TCB_Resume(seL4_CapNull);
ZF_LOGF_IFERR(error, "Failed to start new thread.\n");
如果一切都已正确配置,恢复线程将 result 字符串 Hello2: arg1 0, arg2 0, arg3 0
,后跟一个 fault 。
传递参数
您会注意到新线程的所有参数均为 0。您可以使用辅助函数 sel4utils_arch_init_local_context
或直接操作目标架构的寄存器来设置参数。
练习
更新使用 seL4_TCB_WriteRegisters
写入的值,以分别将值 1、2、3
作为 arg1、arg2 和 arg3
传递。
UNUSED seL4_UserContext regs = {0};
int error = seL4_TCB_ReadRegisters(tcb_cap_slot, 0, 0, sizeof(regs)/sizeof(seL4_Word), ®s);
ZF_LOGF_IFERR(error, "Failed to write the new thread's register set.\n"
"\tDid you write the correct number of registers? See arg4.\n");
sel4utils_arch_init_local_context((void*)new_thread,
(void *)1, (void *)2, (void *)3,
(void *)tcb_stack_top, ®s);
error = seL4_TCB_WriteRegisters(tcb_cap_slot, 0, 0, sizeof(regs)/sizeof(seL4_Word), ®s);
ZF_LOGF_IFERR(error, "Failed to write the new thread's register set.\n"
"\tDid you write the correct number of registers? See arg4.\n");
Resolving a fault
此时,您已经创建并配置了一个新线程,并为其提供了初始参数。 本教程的最后一部分是当您的线程出现故障时该怎么做。 我们将在以后的教程中提供有关故障处理的更多详细信息,但现在您可以依赖内核打印故障消息,因为您创建的线程没有故障处理程序。
在下面的输出中,您可以看到发生了 cap 故障。 错误的第一部分是内核无法将故障发送到故障处理程序,因为它被设置为 (nil)
。 然后内核打印出它试图发送的故障。 在这种情况下,故障是虚拟内存故障。 新线程已尝试访问地址 0x2
处的数据,该地址是无效且未映射的地址。 输出显示线程发生故障时的程序计数器为 0x401e66
。
故障状态寄存器也有输出,可以参考相关架构手册进行解码。
此外,内核会根据当前堆栈指针打印原始堆栈转储。 堆栈转储的大小是可配置的,使用 KernelUserStackTraceLength cmake
变量。
Caught cap fault in send phase at address (nil)
while trying to handle:
vm fault on data at address 0x2 with status 0x4
in thread 0xffffff8008140400 "child of: 'tcb_threads'" at address 0x401e66
With stack:
0x439fc0: 0x0
0x439fc8: 0x3
0x439fd0: 0x2
0x439fd8: 0x1
0x439fe0: 0x0
0x439fe8: 0x1
0x439ff0: 0x0
0x439ff8: 0x0
0x43a000: 0x404fb3
0x43a008: 0x0
0x43a010: 0x0
0x43a018: 0x0
0x43a020: 0x0
0x43a028: 0x0
0x43a030: 0x0
0x43a038: 0x0
排查故障时,可以在加载的ELF文件上使用objdump
等工具查看导致故障的指令。 在这种情况下,ELF 文件位于 ./<BUILD_DIR>/<TUTORIAL_BUILD_DIR>/threads
。
您应该能够看到 arg2 正在被取消引用,但未指向有效内存。
通过传递全局变量的地址来练习传递有效的 arg2。
接下来,另一个错误将发生,因为新线程期望 arg1 是指向函数的指针。
练习
将输出传递给它的参数的函数的地址作为 arg2 传递。
现在您应该有一个新线程,它会立即调用传入 arg2 的函数。