diff --git a/design/multi-cpu-entry.md b/design/multi-cpu-entry.md new file mode 100644 index 0000000000000000000000000000000000000000..2eec4e8fbb1ca3f61463626bec649be30297e914 --- /dev/null +++ b/design/multi-cpu-entry.md @@ -0,0 +1,280 @@ + +|RFC PR链接|RFC Issue链接|作者| +|---------|-------------|----| +|[rfcs-pr#34][rfc-pr#34]|[rfc-issue#IA75YL][rfc-issue#IA75YL]|@tenonos-admin| + +# RFC-NNNN: <通过静态存储用户函数的方式解决Tenon多核无调度场景CPU不能切换线程模型的问题> + + +通过编译期间静态存储用户程序至内存段的方式解决Tenon 多核场景从核任务不能被封装成线程模型问题 + + +- [背景](#背景) + - [目标](#目标) + - [非目标](#非目标) +- [方案提议](#方案提议) + - [使用场景和案例 (可选)](#使用场景和案例) + - [场景案例1](#场景案例1) + - [约束限制说明 (可选)](#约束限制说明) + - [风险点和缓解措施](#风险点和缓解措施) +- [详细设计](#详细设计) + - [功能设计](#功能设计) + - [测试设计](#测试设计) + - [单元测试](#单元测试) +- [缺点](#缺点) + + +## 背景 +TenonOS所有运行程序均以线程形式管理。然而,在轻量化、不支持调度器的场景下,用户为多核预设的任务(线程)无法被调度,多核性能无法被释放。通过在TeonOS编译期将用户为CPU预设的应用程序存储至内存段,可在boot阶段将上述用户程序封装成线程模型,并转发给指定CPU,充分释放多核性能。 + + +### 目标 +1. 为多核无调度器场景下,CPU切换至线程模型提供初期准备: + * 在TeonOS编译期将用户为CPU预设的入口程序(函数)存储至内存段。 + * 支持指定单个CPU绑定入口函数。 + * 支持为连续多个CPU绑定相同应用函数。 + + +### 非目标 +1. 在TenonOS boot阶段将存储的CPU入口点函数封装成线程模型。 +2. 将上述封装好的线程模型分配至指定CPU进行执行。 + + +## 方案提议 +在无调度多核场景下,用户可以为CPU预设入口函数。 +1. 由于需要在编译期将相关信息(入口函数地址、函数参数等)保存,可以使用宏展开的形式静态创建结构体,以存储上述描述的信息。 +2. 在链接文件中增加新的SECTION,用于存储这些结构体。 +3. 用户应为各CPU提供入口点函数,支持绑定CPU。在某些场景下,多个CPU执行相同的任务。针对上述不同场景,需提供不同的API。 + + +### 使用场景和案例 + +#### 场景案例1 +该特性的开发旨在服务多核无调度器场景。该场景下用户在"main.c"中向各CPU注册入口点函数,并将其存储至新增的SECTION段。后续在boot流程中,将上述各CPU的入口点函数封装成线程模型。 + +### 约束限制说明 +1. 编译期宏展开的参数需要是常数或地址等编译器可识别的内容,因此用户需提供全局的函数参数。 + + +### 风险点和缓解措施 + +1. CPU_X的入口点重入问题 +场景:如果多次调用向连续多个 CPU 注册入口点的宏,可能出现CPU_X的入口点重入问题。例如,注册从ID=4开始的2个CPU(CPU=4/5)入口点函数,再注册从ID=5开始的2个CPU(CPU=5/6)入口点函数,会导致CPU(ID=5)重复注册两次入口点。 +解决方式:在后续boot阶段创建线程过程中,增加对重入的校验。 + + +## 详细设计 + + +### 功能设计 +1. 新增SECTION + + 在链接脚本中追加'ENTRYTAB_SECTION'段,用于存放各CPU的入口点结构体。'ENTRYTAB'段起始地址'tn_entrytab_start',结束地址'tn_entrytab_end'。 + +``` +#define ENTRYTAB_SECTION \ + tn_entrytab_start = .; \ + .tn_entrytab : \ + { \ + KEEP(*(SORT_BY_NAME(.tn_entrytab*))) \ + } \ + tn_entrytab_end = .; +``` + +2. 结构体设计 +* tn_lcpu_entry + + 该结构体成员自上到下分别为: + * ep:存储入口函数信息(函数地址,函数参数)。 + * type:根据ep中函数参数个数,划分类型(type=0-2)。 + * cpu_id:ep需要绑核,该字段存储关联LCPU的ID。 + * num:自cpu_id开始的连续N个CPU使用相同的入口点函数,'num = N'。 + +``` +struct tn_lcpu_entry { + entrypoint_t ep; + + entry_type_t type; + + __lcpuid cpu_id; + + uint32_t num; +}; +``` +* entrypoint_t + + TenonOS中线程库开放 'uk_thread_fn0_t'、'uk_thread_fn1_t'、'uk_thread_fn2_t'函数入口。'entrypoint_t'联合体根据入参个数选择相应的函数入口。 +``` +typedef union entrypoint { + tn_lcpu_entry_fn0_t fn0; + tn_lcpu_entry_fn1_t fn1; + tn_lcpu_entry_fn2_t fn2; +} entrypoint_t; + +typedef uk_thread_fn0_t tn_lcpu_entry_fn0_t; + +typedef struct tn_lcpu_entry_fn1 { + uk_thread_fn1_t entry; + void *argp; +} tn_lcpu_entry_fn1_t; + +typedef struct tn_lcpu_entry_fn2 { + uk_thread_fn2_t entry; + void *argp0; + void *argp1; +} tn_lcpu_entry_fn2_t; + +/* Boot阶段可基于type类型(入参个数)调用不同的线程创建接口 */ +typedef enum entry_type { + ENTRY_NO_ARG, + ENTRY_ARG_ONE, + ENTRY_ARG_TWO, +} entry_type_t; +``` +3. API设计 +* 结构体生成宏 + + 以__TN_ENTRYTAB_FN2宏为例,该宏支持两个函数参数。 +``` +#define __TN_ENTRYTAB_FN2(entry_fn, func_argp0, func_argp1, lcpu_id,\ + lcpu_num, prefix)\ + static const struct tn_lcpu_entry \ + __used __section("."prefix #lcpu_id) __align(8) \ + __tn_entrytab_ ## lcpu_id = { \ + .ep.fn2.entry = (entry_fn), \ + .ep.fn2.argp0 = (func_argp0), \ + .ep.fn2.argp1 = (func_argp1), \ + .type = ENTRY_ARG_TWO, \ + .cpu_id = (lcpu_id), \ + .num = (lcpu_num) \ + } +``` +prefix参数用于区分测试代码和执行代码。例如,执行代码使用"tn_entrytab";测试代码使用"tn_test_entrytab"。调用上述宏将创建静态tn_lcpu_entry类型的结构体,其名字由"__tn_entrytab_"和参数lcpu_id拼接而成。__TN_ENTRYTAB_FN2该宏支持两个函数参数,故联合体ep中使用fn2(支持两个参数的结构体),并将宏参数写入各字段。 +__TN_ENTRYTAB_FN0/__TN_ENTRYTAB_FN1 实现与__TN_ENTRYTAB_FN2类似,区别在于函数参数个数不同。 + +* 用户注册API + + 开放两类注册宏,类别一支持向单个CPU注册入口点;类别二支持向CPU_X起的连续N个CPU注册入口点。 + 下分别以tn_lcpu_entrypoint_fn2_register/tn_multi_lcpu_entrypoint_fn2_register宏为例,上述宏均支持两个函数参数。 +``` +#define tn_lcpu_entrypoint_fn2_register(entry_fn, argp0, argp1, lcpu_id)\ + do { \ + UK_ASSERT((lcpu_id) < (int)ukplat_lcpu_count()); \ + __TN_ENTRYTAB_FN2(entry_fn, argp0, argp1, lcpu_id, 1, \ + "tn_entrytab");\ + } while (0) +``` +``` +#define tn_multi_lcpu_entrypoint_fn2_register(entry_fn, \ + argp0, argp1, lcpu_id, num)\ + do { \ + UK_ASSERT(((lcpu_id) + (num) - 1) < \ + (int)ukplat_lcpu_count()); \ + __TN_ENTRYTAB_FN2(entry_fn, argp0, argp1, \ + lcpu_id, num, "tn_entrytab");\ + } while (0) +``` +上述两宏区别仅在于向tn_lcpu_entry结构体中num字段写入的值不同,前者固定为1,后者值由用户决定。 + +* 用于遍历ENTRYTAB_SECTION的辅助宏 + +``` +#define tn_entrytab_foreach(itr, entrytab_start, entrytab_end) \ + for ((itr) = DECONST(struct tn_lcpu_entry *, entrytab_start);\ + (itr) < &(entrytab_end); \ + (itr)++) +``` + +* 测试用的特定API + + 以tn_test_lcpu_entrypoint_fn1_register为例,支持传入一个参数。 +``` +#define tn_test_lcpu_entrypoint_fn1_register(entry_fn, argp, lcpu_id) \ + do { \ + UK_ASSERT((lcpu_id) < (int)ukplat_lcpu_count()); \ + __TN_ENTRYTAB_FN1(entry_fn, \ + argp, lcpu_id, 1, "tn_test_entrytab");\ + } while (0) +``` +该宏与开放给用户的tn_lcpu_entrypoint_fn1_register宏区别仅在于传入prefix字段的值不同(区别见上文),最终生成的结构体将存放在不同的Section中。 + +4. 依赖关系 + +特性开关 CUSTOM_CPU_ENTRYPOINT 仅可以在无调度器多核场景下被打开 +``` +depends on HAVE_SMP && !HAVE_SCHED +``` +### 测试设计 +#### 单元测试 +1. 伪代码 +``` +struct tn_lcpu_entry *entry; +int arg0 = 5; +int arg1 = 7; + +void function_take_one_args(void *arg1 __unused) {} + +void function_take_two_args(void *arg1 __unused, void *arg2 __unused) {} + +UK_TESTCASE_DESC(ukboot_entry, cpu_entry_func_register, +"Register entry functions to the secondary CPUs") +{ + tn_test_lcpu_entrypoint_fn1_register(function_take_one_args, &arg0, 1); + tn_test_multi_lcpu_entrypoint_fn2_register(function_take_two_args, + &arg0, &arg1, 2, 2); + tn_entrytab_foreach(entry, tn_test_entrytab_start, + tn_test_entrytab_end) { + UK_ASSERT(entry); + switch (entry->cpu_id) { + case 1: + UK_TEST_EXPECT_SNUM_EQ(entry->num, 1); + UK_TEST_EXPECT_PTR_EQ(entry->ep.fn1.argp, &arg0); + UK_TEST_EXPECT_SNUM_EQ(entry->type, 1); + UK_TEST_EXPECT_SNUM_EQ(entry->ep.fn1.entry, + function_take_one_args); + break; + case 2: + UK_TEST_EXPECT_SNUM_EQ(entry->num, 2); + UK_TEST_EXPECT_PTR_EQ(entry->ep.fn2.argp0, &arg0); + UK_TEST_EXPECT_PTR_EQ(entry->ep.fn2.argp1, &arg1); + UK_TEST_EXPECT_SNUM_EQ(entry->type, 2); + UK_TEST_EXPECT_SNUM_EQ(entry->ep.fn2.entry, + function_take_two_args); + break; + default: + uk_pr_err("cpu entry unit test failed"); + break; + } + } +} +uk_testsuite_register(ukboot_entry, NULL); +``` +2. 测试方案 + +在TEST_ENTRYTAB_SECTION中注册测试用的结构体,遍历该段中所有结构体,根据cpu_id,来校验各结构体中的字段内容(地址)与预设内容(地址)是否一致。 + + +## 缺点 +1. 为区分测试用例和执行代码,单独划分TEST_ENTRYTAB_SECTION段,仅供该测试用例使用。 +2. 对于不支持全局参数的场景,用户可能需要额外处理传递参数的问题。 + +[rfc-pr#34]: https://gitee.com/tenonos/tenon/pulls/34 +[rfc-issue#IA75YL]: https://gitee.com/tenonos/request-for-comments/issues/IA75YL \ No newline at end of file