查看源代码 C 代码的自动让步

简介

Erlang NIFBIF 不应在没有让步的情况下运行太长时间(在 ERTS 的源代码中通常称为“陷入”)。如果 NIF 和 BIF 占用调度器线程太长时间,Erlang/OTP 系统会变得无响应,并且某些任务可能会被不公平地优先处理。因此,最常用的可能运行很长时间的 NIF 和 BIF 可以让步。

问题

Erlang NIF 和 BIF 通常用 C 编程语言实现。C 编程语言没有内置的在例程中间自动让步的支持(在其他编程语言中称为协程支持)。因此,大多数 NIF 和 BIF 都手动实现让步。手动实现让步的优点是让程序员可以控制应该保存什么以及何时发生让步。不幸的是,手动实现让步还会导致代码中出现大量样板代码,这些代码比不让步的相应代码更难阅读。此外,手动实现让步可能既耗时又容易出错,尤其是在 NIF 或 BIF 复杂的情况下。

解决方案

为了更容易地实现让步的 NIF 和 BIF,创建了一个名为 Yielding C Fun (YCF) 的源到源转换器。YCF 是一个工具,它接受一组函数名和一个 C 源代码文件,并将源代码文件中具有给定名称的函数转换为可让步的版本,这些版本可以用作协程。YCF 的创建考虑到了让步的 NIF 和 BIF,并具有在实现让步的 NIF 和 BIF 时可能很方便的几个功能。建议读者查看 YCF 的文档,以了解 YCF 的详细描述。

Yielding C Fun 的源代码和文档

YCF 的源代码包含在 Erlang/OTP 系统源代码树内的 "$ERL_TOP"/erts/lib_src/yielding_c_fun/ 文件夹中。YCF 的文档可以在 "$ERL_TOP"/erts/lib_src/yielding_c_fun/README.md 中找到。YCF 文档的渲染版本可以在此处找到。

Erlang 运行时系统中的 Yielding C Fun

在撰写本文时,YCF 在 ERTS 中用于以下方面:

ERTS 中 YCF 的最佳实践

首先,在开始使用 YCF 之前,建议通读 erts/lib_src/yielding_c_fun/README.md 中的文档,以了解 YCF 的限制和功能。

标记 YCF 转换的函数

重要的是,能够很容易地看到哪些函数被 YCF 转换了,以便编辑这些函数的程序员知道他们必须遵循某些限制。明确这一点的约定是在函数上方添加注释,解释该函数由 YCF 转换(例如,请参见 erl_map.c 中的 maps_values_1_helper)。如果仅使用函数的转换版本,则约定是通过用以下 #ifdef 将其包围来“注释掉”函数的源代码(这样,就不会收到有关未使用函数的警告)

#ifdef INCLUDE_YCF_TRANSFORMED_ONLY_FUNCTIONS
void my_fun() {
    ...
}
#endif /* INCLUDE_YCF_TRANSFORMED_ONLY_FUNCTIONS */

在编辑函数时,可以定义 INCLUDE_YCF_TRANSFORMED_ONLY_FUNCTIONS,以便可以在未转换的源代码中看到错误和警告。

将 YCF 转换的函数放在哪里

约定是将 YCF 转换的函数的未转换源代码放在它们自然所属的源文件中。例如,map BIF 的函数与其他的 map 相关函数一起放在 erl_map.c 中。在构建时,将调用 YCF 以将函数的转换版本生成到头文件中,该头文件包含在包含函数未转换版本的源文件中(在 $ERL_TOP/erts/emulator/Makefile.in 中搜索 YCF 以查看如何调用 YCF 的示例)。

如果一个由一个 YCF 调用转换的函数 F1 依赖于另一个由 YCF 调用转换的函数 F2,则需要告诉 YCF F2 是一个 YCF 转换的函数,以便 F1 可以调用转换后的版本(请参阅 YCF 的文档中有关如何执行此操作的详细信息,了解 -fexternal 的文档)。

使用 erts_ycf_trap_driver 减少样板代码

erts_ycf_trap_driver 是一个 C 函数,它实现了所有使用 YCF 进行让步的 BIF 所需的通用代码。建议在可能的情况下使用此函数。学习如何使用 erts_ycf_trap_driver 的一个好方法是查看 BIF maps:from_keys/2, maps:from_list/1, maps:keys/1maps:values/1 的实现。

某些 BIF 可能无法使用 erts_ycf_trap_driver,因为它们需要在让步后执行一些自定义工作。例如,BIF ets:insert/2ets:insert_new/2 在 ETS 表结构中发布让步状态,以便其他线程可以帮助完成操作。

测试和查找 YCF 生成的代码中的问题

测试具有手动让步和 YCF 生成的让步的代码的一个好方法是编写测试用例,这些测试用例覆盖代码可以进行让步的位置(让步点),并设置让步限制,以便每次达到让步点时都进行让步。对于 YCF,可以通过将指向值 1 的指针作为 ycf_nr_of_reductions 参数(即,*_ycf_gen_yielding*_ycf_gen_continue 函数的第一个参数)传递来实现。

YCF 标志 -debug 使 YCF 生成在让步时检查指向 C 堆栈的指针的代码。当找到此类指针时,将打印出找到的指针的位置,并且程序将崩溃。当将现有的 C 代码移植到 YCF 进行让步时,这可以节省大量时间!为了使 -debug 选项按预期工作,必须在调用 YCF 生成的函数之前告诉 YCF 堆栈的起始位置。创建了函数 ycf_debug_set_stack_startycf_debug_reset_stack_start,以使其更容易(请参见 erts_ycf_trap_driver 的实现,了解如何使用这些函数)。建议设置 ERTS 的构建,以便 ERTS 的调试版本在运行时使用用 -debug 标志生成的 YCF 代码,而生产代码在运行时使用没有 -debug 标志生成的 YCF 代码。

一个好的做法是浏览 YCF 生成的代码,以尝试查找未正确转换的内容。在执行此操作之前,应使用自动源代码格式化程序格式化生成的代码(否则生成的代码将非常难以阅读)。如果 YCF 没有正确转换某些内容,则几乎可以通过重写代码来修复它(请参阅 YCF 文档,了解支持的内容和不支持的内容)。例如,如果您有一个内联结构变量声明(例如,struct {int field1; int field2;} mystructvar;),则 YCF 不会将此识别为变量声明,但是您可以通过为该结构创建一个 typedef 来修复此问题。

YCF 的钩子在调试由 YCF 转换的代码时很有用。例如,这些钩子可用于在让步时和在让步后恢复时打印变量的值。

不幸的是,YCF 不能很好地处理具有语法错误的 C 代码,并且当给定语法不正确的 C 代码(例如,缺少括号)时可能会崩溃或产生错误的输出,而不会给出任何有用的错误消息。因此,建议在用 YCF 转换代码之前始终使用普通的 C 编译器检查代码。

常见陷阱

  • 指向堆栈的指针 当一个让出的函数继续执行时,堆栈可能位于其他位置,因此指向位于堆栈上的变量的指针可能会成为问题。如前一节所述,-debug 选项是检测此类指针的好方法。YCF 具有使移植包含指向堆栈的指针的代码更容易的功能(有关更多信息,请参阅 YCF 文档中 YCF_STACK_ALLOC 的文档)。另一种修复指向堆栈的指针的方法(有时可能很方便)是使用 YCF 的钩子,在让出的函数恢复时正确设置指向堆栈的指针。

  • YCF 不会展开宏,因此被宏“隐藏”的变量声明、返回语句和 goto 等可能会成为问题。因此,明智的做法是检查 YCF 转换的代码中的所有宏,以确保它们不包含 YCF 需要转换的任何内容。

  • 让出代码中的内存分配 如果在执行让出的 BIF 时进程被终止,则必须确保释放让出代码分配的内存和其他资源。例如,可以通过从持有对陷阱状态引用的魔术二进制文件的 dtor 调用生成的 *_ycf_gen_destroy 函数来完成此操作。当函数的 *_ycf_gen_destroy 函数执行时,可以使用 YCF 的 ON_DESTROY_STATEON_DESTROY_STATE_OR_RETURN 钩子来释放在让出函数内部手动分配的任何资源。erts_ycf_trap_driver 负责调用 *_ycf_gen_destroy 函数,因此如果您使用 erts_ycf_trap_driver,则无需担心这个问题。