有没有办法使用setjmp
和longjmp
函数
答案 0 :(得分:12)
你的确可以。有几种方法可以实现它。困难的部分是最初获得指向其他堆栈的jmpbufs。 Longjmp仅针对由setjmp创建的jmpbuf参数定义,因此如果不使用汇编或利用未定义的行为,就无法执行此操作。用户级线程本质上是不可移植的,因此可移植性并不是不能真正做到这一点的有力论据。
第1步 你需要一个存储不同线程的上下文的地方,所以为你想要的许多线程创建一个jmpbuf结构的队列。
第2步 您需要为每个线程malloc堆栈。
第3步 你需要得到一些jmpbuf上下文,它们在你刚分配的内存位置有堆栈指针。您可以检查计算机上的jmpbuf结构,找出它存储堆栈指针的位置。调用setjmp然后修改其内容,以便堆栈指针位于您分配的堆栈中。堆栈通常会长大,因此您可能希望堆栈指针位于最高内存位置附近。如果你编写一个基本的C程序并使用调试器来反汇编它,然后找到它从函数返回时执行的指令,你就可以找出偏移应该是什么。例如,使用x86上的系统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不保存浮点寄存器状态或标志寄存器,当程序异步中断时是必需的。
答案 1 :(得分:8)
它可能会稍微弯曲规则,但GNU pth会这样做。这是可能的,但你可能不应该自己尝试,除非作为一个学术概念验证练习,如果你想认真地并以一种远程便携的方式使用pth实现 - 你会明白为什么当你阅读第p个线程创建代码。
(基本上它使用一个信号处理程序来欺骗操作系统创建一个新的堆栈,然后longjmp离开那里并保持堆栈。显然它可以工作,但它很粗略。)
在生产代码中,如果您的操作系统支持makecontext / swapcontext,请改用它们。如果它支持CreateFiber / SwitchToFiber,请改用它们。并且要注意令人失望的事实,即最引人注目的使用协程之一 - 即通过外部代码调用的事件处理程序来反转控制 - 是不安全的,因为调用模块必须是可重入的,而且你通常可以不能证明这一点。这就是.NET中不支持光纤的原因......
答案 2 :(得分:3)
这是所谓的用户空间上下文切换的一种形式。
这可能但容易出错,特别是如果你使用setjmp和longjmp的默认实现。这些函数的一个问题是,在许多操作系统中,它们只会保存64位寄存器的子集,而不是整个上下文。这通常是不够的,例如在处理系统库时(我的经验是使用amd64 / windows的自定义实现,所有事情都考虑得非常稳定)。
那就是说,如果你不是在尝试使用复杂的外部代码库或事件处理程序,并且你知道自己在做什么,并且(特别是)如果你在汇编程序中编写自己的版本可以节省更多的当前上下文(如果你使用32位windows或linux这可能没有必要,如果你使用某些版本的BSD我想象它几乎肯定是),你调试它时要特别注意反汇编输出,然后你就可以了实现你想要的。
答案 3 :(得分:2)
为了学习,我做了这样的事情。 https://github.com/Kraego/STM32L476_MiniOS/blob/main/Usercode/Concurrency/scheduler.c
上下文/线程切换由 setjmp/longjmp 完成。困难的部分是让分配的堆栈正确(请参阅allocateStack()),这取决于您的平台。
这只是一个演示如何工作,我永远不会在生产中使用它。
答案 4 :(得分:1)
正如Sean Ogden已经提到的, longjmp()不适合多任务处理,如 它只能向上移动堆栈而且不能 在不同的堆栈之间跳转。不要那样。
如user414736所述,您可以使用getcontext / makecontext / swapcontext 功能,但问题是那些 它们不完全处于用户空间。他们其实 调用sigprocmask()系统调用,因为它们会切换 信号掩码作为上下文切换的一部分。 这使得swapcontext()比longjmp()慢得多, 而且你可能不想要缓慢的共同惯例。
据我所知,没有POSIX标准的解决方案
这个问题,所以我从不同的编译自己
可用来源。您可以找到上下文操作
这里从libtask中提取的函数:
https://github.com/stsp/dosemu2/tree/devel/src/arch/linux/mcontext
功能是:
getmcontext(),setmcontext(),makemcontext()和swapmcontext()。
它们具有与具有相似名称的标准函数类似的语义,
但是他们也模仿了getmcontext()中的setjmp()语义
当被setmcontext()跳转到时,返回1(而不是0)。
最重要的是,您可以使用libpcl的端口,协程库:
https://github.com/stsp/dosemu2/tree/devel/src/base/misc/libpcl
这样,可以实现快速协作用户空间
穿线。它适用于Linux,i386和x86_64拱门。