确实可以。有几种方法可以实现它。困难的部分是最初获取指向其他堆栈的 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 寄存器,在程序被异步中断时是必需的。