使用setjmp,longjmp实现多任务处理

21

是否有使用setjmplongjmp函数来实现多任务处理的方法?


1
Tony Finch的picoro(小协程)是一种协作式多任务处理技术,其协程源自于Knuth的《计算机程序设计艺术》。此外,Simon Tatham还有一个协程网页,提供了很好的解释。您可以通过以下链接访问:http://www.chiark.greenend.org.uk/~sgtatham/coroutines.html - artless noise
同时,需要注意的是,setjmp()longjmp()通常/总是以汇编语言实现,并类似于操作系统上下文切换代码。然而,它们可能无法保存某些状态,例如浮点数SIMD状态等。这是实现错误还是标准问题,我不知道。然而,在实践中,这个问题经常存在。了解要保存哪些状态可以显著提高上下文切换速度。 - artless noise
请参阅setjmp()和fpmode以了解其他CPU状态的更多信息。 - artless noise
5个回答

14

确实可以。有几种方法可以实现它。困难的部分是最初获得jmpbufs指向其他堆栈的方式。longjmp仅为由setjmp创建的jmpbuf参数定义,因此除了使用汇编或利用未定义行为之外,没有办法做到这一点。用户级线程本质上是不可移植的,因此在不这样做的情况下,可移植性并非一个强有力的论据。

步骤1 您需要一个存储不同线程上下文的地方,因此为所需的线程制作jmpbuf结构的队列。

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

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

您还可以编写一些非常小(一行)的内联汇编代码来操作堆栈指针,然后调用setjmp。实际上,这更具可移植性,因为在许多系统中,jmpbuf中的指针被混淆以进行安全保护,因此您无法轻松修改它们。

我没有尝试过,但你可以通过声明一个非常大的数组并因此移动堆栈指针来避免汇编语言。

第四步 你需要让现有线程返回系统到某个安全状态。如果不这样做,而其中一个线程返回,则会将分配的堆栈正上方的地址作为返回地址,并跳转到某个垃圾位置,很可能导致段错误。所以首先你需要一个安全的返回位置。在主线程中调用 setjmp 并将 jmpbuf 存储在全局可访问位置中来实现此目的。定义一个不带参数的函数,只需调用 longjmp 并使用保存的全局 jmpbuf。获取该函数的地址并将其复制到您留出返回地址的分配堆栈中。可以将帧指针保留为空。现在,当线程返回时,它将转到调用 longjmp 的该函数,然后立即跳回到主线程,您在其中调用了 setjmp,每次都是如此。

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

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

结论 这是基础。只要线程继续调用上下文切换,队列就会不断地重新填充,并且不同的线程会运行。当一个线程返回时,如果还有线程可以运行,则主线程选择其中一个线程,如果没有线程了,则进程终止。通过相对较少的代码,您可以拥有一个相当基本的协作式多任务设置。您可能还需要执行其他一些操作,例如实现清理函数以释放已死线程的堆栈等。您还可以使用信号来实现抢占,但这更加困难,因为 setjmp 不保存浮点寄存器状态或标志寄存器状态,在程序被异步中断时这些状态是必需的。


一些setjmp/longjmp的特定实现可能会以所需的方式工作,可以使它们产生失败,并且有可能一些编译器甚至规定他们的实现以一种允许这样做的方式工作,而不必依赖于针对此类编译器时未经记录/未定义的行为,但我建议使用几行汇编代码来进行堆栈/寄存器切换。 使用setjmp/longjmp与汇编代码一样具有可移植性,但可能会给出可移植性的错觉。 - supercat
话虽如此,我认为合作式多任务有很多值得称赞的地方。许多编译器专门记录外部汇编语言模块需要保留哪些寄存器(如果有的话)。强制性抢占式多任务必须要保留编译器可能使用的所有寄存器,如果编译器利用了多任务不知道的硬件乘法加速单元,这可能会成为一个问题,但是合作式多任务避免了这些问题。话虽如此... - supercat
1
诸如C++异常之类的东西,根据它们的实现方式,可能与协作式多任务处理甚至不兼容。人们必须研究异常是如何实现的才能知道正在运行的线程所维护的堆栈需要什么。 - supercat
1
如果您使用堆栈溢出方法来调整堆栈指针,就不需要汇编语言,并且该方法适用于所有堆栈向下增长且堆栈帧以RA和FP开头的机器(需要使用sizeof(int*)获取偏移量的适当大小)。这几乎涵盖了所有使用Windows、OSX或Linux的x86/AMD64机器。 - Sean Ogden
1
许多C++实现使用一个或多个静态变量来控制异常处理;尝试在使用异常的线程之间切换将需要任务切换器了解这些变量并交换它们。交换它们并不难——确保没有任何未知的静态变量才是困难的部分。 - supercat
显示剩余4条评论

8
它可能会有点违反规则,但GNU pth可以做到这一点。如果您想认真地并且在远程可移植的方式下进行操作,请使用pth实现--当您阅读pth线程创建代码时,您就会明白为什么了,但除了学术概念验证之外,您可能不应该自己尝试它。
(基本上,它使用信号处理程序来欺骗操作系统创建一个新的堆栈,然后longjmp跳出并保留堆栈。显然它能够工作,但是它非常不可靠。)
在生产代码中,如果您的操作系统支持makecontext/swapcontext,请使用它们。如果它支持CreateFiber/SwitchToFiber,请使用它们。还要注意令人失望的事实,即协程的最引人注目的用途之一--通过从由外部代码调用的事件处理程序中yield来反转控制--是不安全的,因为调用模块必须是可重入的,而您通常无法证明这一点。这就是为什么.NET仍不支持纤程的原因...

2
Netscape Portable Runtime(NSPR)似乎还定义了一些宏,使用更简单但更复杂的方法来完成这个过程:它们只是调用setjmp,然后在缓冲区中更改机器堆栈指针和指令指针。谷歌“_MD_INIT_CONTEXT”以获得有趣的阅读体验。 - j_l

5

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

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

话虽如此,如果您不打算使用复杂的外部代码库或事件处理程序,并且知道自己在做什么,特别是如果您编写自己的汇编版本以保存更多当前上下文(如果您使用32位windows或linux,则可能不需要,如果您使用某些BSD版本,则我想几乎肯定需要),并且在调试时仔细注意反汇编输出,则可能能够实现您想要的功能。


2

1

正如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()。它们与名称类似的标准函数具有相似的语义,但也模仿了setjmp()语义,即当由setmcontext()跳转时,getmcontext()返回1(而不是0)。

除此之外,您还可以使用libpcl的端口,该协程库:
https://github.com/dosemu2/dosemu2/tree/devel/src/base/lib/libpcl
通过使用它,可以实现快速的用户空间协作线程。它适用于Linux,i386和x86_64架构。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接