有没有办法使用setjmp
和longjmp
函数来实现多任务处理
5 回答
你确实可以。有几种方法可以实现它。困难的部分是最初获取指向其他堆栈的 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 不保存浮点寄存器状态或标志寄存器,这是程序异步中断时所必需的。
它可能会稍微改变规则,但 GNU pth 会这样做。这是可能的,但您可能不应该自己尝试,除非作为学术概念验证练习,如果您想认真地以远程可移植的方式执行它,请使用 pth 实现——当您阅读时您会明白为什么第 p 个线程创建代码。
(本质上它使用一个信号处理程序来欺骗操作系统创建一个新的堆栈,然后 longjmp 离开那里并保留堆栈。它显然可以工作,但它很粗略。)
在生产代码中,如果您的操作系统支持 makecontext/swapcontext,请改用它们。如果它支持 CreateFiber/SwitchToFiber,请改用它们。请注意一个令人失望的事实,即协程最引人注目的用途之一——即通过让出由外部代码调用的事件处理程序来反转控制——是不安全的,因为调用模块必须是可重入的,而且您通常可以不能证明这一点。这就是为什么 .NET 仍然不支持光纤的原因......
这是所谓的用户空间上下文切换的一种形式。
这是可能的,但很容易出错,特别是如果您使用 setjmp 和 longjmp 的默认实现。这些函数的一个问题是,在许多操作系统中,它们只会保存 64 位寄存器的子集,而不是整个上下文。这通常是不够的,例如在处理系统库时(我在这里的经验是使用 amd64/windows 的自定义实现,考虑到所有事情都非常稳定)。
也就是说,如果您不尝试使用复杂的外部代码库或事件处理程序,并且您知道自己在做什么,并且(尤其是)如果您在汇编程序中编写自己的版本以保存更多当前上下文(如果您'正在使用 32 位 windows 或 linux 这可能没有必要,如果您使用某些版本的 BSD,我想几乎肯定是这样),并且您调试它并仔细注意反汇编输出,那么您可能能够实现你要。
我为学习做了这样的事情。 https://github.com/Kraego/STM32L476_MiniOS/blob/main/Usercode/Concurrency/scheduler.c
上下文/线程切换由 setjmp/longjmp 完成。困难的部分是正确分配堆栈(请参阅 allocateStack()),这取决于您的平台。
这只是一个演示如何工作,我永远不会在生产中使用它。
正如 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() 语义,即 getmcontext() 在被 setmcontext() 跳转到时返回 1(而不是 0)。
最重要的是可以使用libpcl的一个端口,协程库:
https
://github.com/dosemu2/dosemu2/tree/devel/src/base/lib/libpcl
有了这个,就可以实现快速协同用户空间线程。它适用于 linux、i386 和 x86_64 拱门。