是否有使用setjmp
和longjmp
函数来实现多任务处理的方法?
确实可以。有几种方法可以实现它。困难的部分是最初获得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的默认实现尤其容易出错。其中一个问题是,在许多操作系统中,它们只保存64位寄存器的子集,而不是整个上下文。这通常是不够的,例如在处理系统库时(我的经验是在amd64 / windows的自定义实现中,考虑到所有事情,工作得非常稳定)。
话虽如此,如果您不打算使用复杂的外部代码库或事件处理程序,并且知道自己在做什么,特别是如果您编写自己的汇编版本以保存更多当前上下文(如果您使用32位windows或linux,则可能不需要,如果您使用某些BSD版本,则我想几乎肯定需要),并且在调试时仔细注意反汇编输出,则可能能够实现您想要的功能。
正如Sean Ogden所提到的那样,longjmp()不适合多任务处理,因为它只能将堆栈向上移动,不能在不同的堆栈之间跳转。这是不可取的。
正如user414736所提到的那样,您可以使用getcontext/makecontext/swapcontext函数,但问题在于它们并非完全处于用户空间。它们实际上调用了sigprocmask()系统调用,因为它们在上下文切换时会切换信号掩码。这使得swapcontext()比longjmp()慢得多,而您可能不希望使用缓慢的协程。
据我所知,目前没有符合POSIX标准的解决方案来解决这个问题,因此我从不同的可用源代码编译了自己的解决方案。您可以在此处找到从libtask中提取的上下文操作函数:除此之外,您还可以使用libpcl的端口,该协程库:
https://github.com/dosemu2/dosemu2/tree/devel/src/base/lib/libpcl
通过使用它,可以实现快速的用户空间协作线程。它适用于Linux,i386和x86_64架构。
setjmp()
和longjmp()
通常/总是以汇编语言实现,并类似于操作系统上下文切换代码。然而,它们可能无法保存某些状态,例如浮点数、SIMD状态等。这是实现错误还是标准问题,我不知道。然而,在实践中,这个问题经常存在。了解要保存哪些状态可以显著提高上下文切换速度。 - artless noisesetjmp()
和fpmode以了解其他CPU状态的更多信息。 - artless noise