官方教程:SeL4: 线程

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_MinPrioseL4_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_Uint8domain新的调度域
seL4_TCBthread要配置的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

当您第一次构建并运行本教程时,您应该会看到如下内容:

NEIR
释放所有 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 对象,将其配置为与当前线程具有相同的 CSpaceVSpace。 使用我们提供的 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), &regs);
    ZF_LOGF_IFERR(error, "Failed to read the new thread's register set.\n");

// TODO 使用有效的指令指针
    sel4utils_set_instruction_pointer(&regs, (seL4_Word)NULL);
// TODO 使用有效的堆栈指针
    sel4utils_set_stack_pointer(&regs, NULL);
// TODO 修复此调用的参数
    error = seL4_TCB_WriteRegisters(seL4_CapNull, 0, 0, 0, &regs);
    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), &regs);
    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, &regs);
    error = seL4_TCB_WriteRegisters(tcb_cap_slot, 0, 0, sizeof(regs)/sizeof(seL4_Word), &regs);
    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 的函数。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值