【问题标题】:Multitasking using setjmp, longjmp使用 setjmp、longjmp 进行多任务处理
【发布时间】:2010-04-01 13:50:13
【问题描述】:

有没有办法使用setjmplongjmp 函数实现多任务处理

【问题讨论】:

  • Tony Finch's picoro(small co-routines)。协同程序属于 Knuth 的计算艺术,是协作式多任务处理。同样,Simon Tatham 有一个 co-routines web page 并提供了很好的解释。
  • 另外,应该小心; setjmp()longjmp() 最经常/总是在汇编程序中实现,类似于 OS 上下文切换代码。但是,它们可能不会保存一些状态,例如 浮点SIMD 状态 等。这是实现错误还是标准问题,我不知道。但是,这个问题在实践中经常会存在。知道要保存什么状态可以显着提高上下文切换速度。
  • 有关其他 CPU 状态的更多信息,请参阅:setjmp() and fpmode

标签: setjmp


【解决方案1】:

确实可以。有几种方法可以实现它。困难的部分是最初获取指向其他堆栈的 jmpbufs。 Longjmp 仅针对由 setjmp 创建的 jmpbuf 参数定义,因此如果不使用程序集或利用未定义的行为,就无法做到这一点。用户级线程本质上是不可移植的,因此可移植性并不是真正不这样做的有力论据。

第 1 步 你需要一个地方来存储不同线程的上下文,所以为你想要的多个线程创建一个 jmpbuf 结构队列。

第 2 步 您需要为每个线程分配一个堆栈。

第 3 步 您需要获取一些 jmpbuf 上下文,这些上下文在您刚刚分配的内存位置中具有堆栈指针。您可以检查机器上的 jmpbuf 结构,找出它存储堆栈指针的位置。调用 setjmp 然后修改其内容,使堆栈指针位于您分配的堆栈之一中。堆栈通常会向下增长,因此您可能希望堆栈指针靠近最高内存位置。如果您编写一个基本的 C 程序并使用调试器对其进行反汇编,然后找到它在您从函数返回时执行的指令,您可以找出偏移量应该是多少。例如,使用 x86 上的 system V 调用约定,您会看到它弹出 %ebp(帧指针),然后调用 ret 将返回地址弹出堆栈。因此,在进入函数时,它会推送返回地址和帧指针。每次推送都会将堆栈指针向下移动 4 个字节,因此您希望堆栈指针从已分配区域的高地址 -8 个字节开始(就像您刚刚调用了一个函数一样)。接下来我们将填充 8 个字节。

您可以做的另一件事是编写一些非常小的(一行)内联汇编来操作堆栈指针,然后调用 setjmp。这实际上更具可移植性,因为在许多系统中 jmpbuf 中的指针为了安全而被破坏,因此您不能轻易修改它们。

我还没有尝试过,但是您可以通过声明一个非常大的数组并因此移动堆栈指针来故意溢出堆栈来避免 asm。

第 4 步 您需要退出线程才能将系统返回到某个安全状态。如果您不这样做,并且其中一个线程返回,它将把您分配的堆栈上方的地址作为返回地址并跳转到某个垃圾位置并可能出现段错误。所以首先你需要一个安全的地方返回。通过在主线程中调用 setjmp 并将 jmpbuf 存储在全局可访问的位置来获取此信息。定义一个不带参数的函数,只使用保存的全局 jmpbuf 调用 longjmp。获取该函数的地址并将其复制到您为返回地址留出空间的分配堆栈中。您可以将帧指针留空。现在,当一个线程返回时,它会转到调用 longjmp 的那个函数,然后每次都直接跳回调用 setjmp 的主线程。

第 5 步 在主线程的 setjmp 之后,您需要一些代码来确定接下来要跳转到哪个线程,从队列中拉出适当的 jmpbuf 并调用 longjmp 到那里。当该队列中没有剩余线程时,程序完成。

第 6 步 编写一个上下文切换函数,调用 setjmp 并将当前状态存储回队列中,然后将 longjmp 存储在队列中的另一个 jmpbuf 上。

结论 这就是基础。只要线程不断调用上下文切换,队列就会不断重新填充,并运行不同的线程。当一个线程返回时,如果还有剩余可以运行,则由主线程选择一个,如果没有剩余,则进程终止。使用相对较少的代码,您就可以拥有一个非常基本的协作式多任务设置。您可能想要做更多的事情,例如实现清理函数以释放死线程的堆栈等。您还可以使用信号实现抢占,但这要困难得多,因为 setjmp 不保存浮点寄存器state 或 flags 寄存器,在程序被异步中断时是必需的。

【讨论】:

  • setjmp/longjmp 的某些特定实现可能会以这样一种方式工作,以至于人们可以将它们作为所需的行为,并且某些编译器甚至可能指定它们的实现以特定方式工作,在针对此类编译器时,无需依赖未记录/未定义的行为即可实现此类操作,但我建议改为使用几行汇编代码来执行堆栈/寄存器切换。使用 setjmp/longjmp 并不比汇编代码更具可移植性,但可能会产生可移植性的错觉。
  • 话虽如此,我认为对于协作式多任务处理还有很多话要说。许多编译器明确记录了外部汇编语言模块需要保留哪些寄存器(如果有)。抢先式多任务程序必须保留编译器可能正在使用的所有寄存器,这可能是一个问题,例如编译器利用了多任务处理程序不知道的硬件乘法加速加速单元,但协作式多任务处理程序避免了此类问题。说了这么多……
  • ...C++ 异常之类的东西,取决于它们的实现方式,可能会也可能不会与协作式多任务处理相得益彰。必须研究异常是如何实现的,才能知道运行线程维护的堆栈需要什么。
  • 如果使用堆栈溢出的方法调整堆栈指针,没有asm,并且该方法适用于堆栈向下增长并且堆栈帧以RA和FP开头的所有机器(需要使用 sizeof(int*) 为偏移量获取适当的大小)。这几乎涵盖了所有使用 Windows、OSX 或 Linux 的 x86/AMD64 机器。
  • 很多C++实现使用一个或多个静态变量来控制异常处理;尝试在使用异常的线程之间切换将需要任务切换器知道这些变量并交换它们。交换它们并不难——确保没有任何不知道的静态变量是困难的部分。
【解决方案2】:

这可能有点违反规则,但 GNU pth 会这样做。这是可能的,但您可能不应该自己尝试,除非作为学术概念验证练习,如果您想认真地以远程可移植的方式执行它,请使用 pth 实现——当您阅读时您会明白为什么第 p 个线程创建代码。

(本质上,它使用信号处理程序来欺骗操作系统创建一个新堆栈,然后 longjmp 离开那里并保留堆栈。显然它可以工作,但它很粗略。)

在生产代码中,如果您的操作系统支持 makecontext/swapcontext,请改用它们。如果它支持 CreateFiber/SwitchToFiber,请改用它们。请注意一个令人失望的事实,即协程最引人注目的用途之一——即通过让出由外部代码调用的事件处理程序来反转控制——是不安全的,因为调用模块必须是可重入的,而且您通常可以不能证明这一点。这就是为什么 .NET 仍然不支持光纤的原因...

【讨论】:

  • Netscape 可移植运行时 (NSPR) 似乎也使用更简单但更复杂的方法定义了用于执行此操作的宏:它们只需调用 setjmp,然后更改缓冲区中的机器堆栈指针和指令指针。谷歌“_MD_INIT_CONTEXT”阅读有趣。
【解决方案3】:

这是一种称为用户空间上下文切换的形式。

这是可能的,但容易出错,特别是如果您使用 setjmp 和 longjmp 的默认实现。这些函数的一个问题是,在许多操作系统中,它们只会保存 64 位寄存器的子集,而不是整个上下文。这通常是不够的,例如在处理系统库时(我在这里的经验是使用 amd64/windows 的自定义实现,它在所有考虑的情况下都非常稳定)。

也就是说,如果您不尝试使用复杂的外部代码库或事件处理程序,并且您知道自己在做什么,并且(尤其是)如果您在汇编程序中编写自己的版本以节省更多当前上下文(如果您使用的是 32 位 Windows 或 linux,这可能没有必要,如果您使用 BSD 的某些版本,我想它几乎肯定是),并且您调试它并仔细注意反汇编输出,那么您可能能够达到你想要的。

【讨论】:

    【解决方案4】:

    我为学习做了这样的事情。 https://github.com/Kraego/STM32L476_MiniOS/blob/main/Usercode/Concurrency/scheduler.c

    上下文/线程切换由setjmp/longjmp完成。困难的部分是正确分配堆栈(请参阅 allocateStack()),这取决于您的平台。

    这只是一个演示如何工作,我永远不会在生产中使用它。

    【讨论】:

      【解决方案5】:

      正如 Sean Ogden 已经提到的, longjmp() 不适合多任务处理,因为 它只能向上移动堆栈而不能 在不同的堆栈之间跳转。没办法。

      正如 user414736 所说,您可以使用 getcontext/makecontext/swapcontext 功能,但问题在于 它们并不完全在用户空间中。他们实际上 调用 sigprocmask() 系统调用,因为它们切换 信号掩码作为上下文切换的一部分。 这使得 swapcontext() 比 longjmp() 慢得多, 而且您可能不想要缓慢的协同程序。

      据我所知,没有 POSIX 标准的解决方案 这个问题,所以我从不同的地方编译了我自己的 可用来源。你可以找到上下文操作 此处从 libtask 中提取的函数:
      https://github.com/dosemu2/dosemu2/tree/devel/src/base/lib/mcontext
      功能是: getmcontext()、setmcontext()、makemcontext() 和 swapmcontext()。 它们与具有相似名称的标准函数具有相似的语义, 但它们也模仿了 getmcontext() 中的 setjmp() 语义 通过 setmcontext() 跳转时返回 1(而不是 0)。

      除此之外,您还可以使用 libpcl 的一个端口,即协程库:
      https://github.com/dosemu2/dosemu2/tree/devel/src/base/lib/libpcl
      有了这个,可以实现快速协作用户空间 穿线。它适用于 linux、i386 和 x86_64 架构。

      【讨论】:

        猜你喜欢
        • 2010-10-23
        • 2011-06-25
        • 1970-01-01
        • 2016-11-09
        • 1970-01-01
        • 1970-01-01
        • 2015-12-10
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多