使用 setjmp、longjmp 进行多任务处理

发布于 2024-08-27 12:56:21 字数 63 浏览 5 评论 0原文

有没有办法使用setjmplongjmp函数实现多任务处理

is there a way to implement multitasking using setjmp and longjmp functions

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

滥情空心 2024-09-03 12:56:21

你确实可以。有几种方法可以实现它。困难的部分是最初获取指向其他堆栈的 jmpbufs。 Longjmp 仅为由 setjmp 创建的 jmpbuf 参数定义,因此如果不使用程序集或利用未定义的行为,就无法做到这一点。用户级线程本质上是不可移植的,因此可移植性并不是不这样做的有力理由。

第 1 步
您需要一个地方来存储不同线程的上下文,因此为您想要的任意数量的线程创建一个 jmpbuf 结构队列。

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

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

您可以做的另一件事是编写一些非常小的(一行)内联程序集来操作堆栈指针,然后调用 setjmp。这实际上更可移植,因为在许多系统中,为了安全起见,jmpbuf 中的指针被破坏,因此您无法轻松修改它们。

我还没有尝试过,但您可能可以通过声明一个非常大的数组并移动堆栈指针来故意溢出堆栈来避免汇编。

第 4 步
您需要退出线程才能将系统返回到某种安全状态。如果您不这样做,并且其中一个线程返回,它将采用分配的堆栈上方的地址作为返回地址,并跳转到某个垃圾位置并可能出现段错误。所以首先你需要一个安全的地方可以返回。通过在主线程中调用 setjmp 并将 jmpbuf 存储在全局可访问的位置来获取此信息。定义一个不带参数的函数,仅使用保存的全局 jmpbuf 调用 longjmp。获取该函数的地址并将其复制到分配的堆栈中,为返回地址留出空间。您可以将帧指针留空。现在,当线程返回时,它将转到调用 longjmp 的函数,并每次都直接跳回您调用 setjmp 的主线程。

第5步
在主线程 setjmp 之后,您需要一些代码来确定下一个跳转到哪个线程,将适当的 jmpbuf 从队列中拉出并调用 longjmp 转到那里。当该队列中没有剩余线程时,程序就完成了。

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

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

You can indeed. There are a couple of ways to accomplish it. The difficult part is initially getting the jmpbufs which point to other stacks. Longjmp is only defined for jmpbuf arguments which were created by setjmp, so there's no way to do this without either using assembly or exploiting undefined behavior. User level threads are inherently not portable, so portability isn't a strong argument for not doing it really.

step 1
You need a place to store the contexts of different threads, so make a queue of jmpbuf stuctures for however many threads you want.

Step 2
You need to malloc a stack for each of these threads.

Step 3
You need to get some jmpbuf contexts which have stack pointers in the memory locations you just allocated. You could inspect the jmpbuf structure on your machine, find out where it stores the stack pointer. Call setjmp and then modify its contents so that the stack pointer is in one of your allocated stacks. Stacks usually grow down, so you probably want your stack pointer somewhere near the highest memory location. If you write a basic C program and use a debugger to disassemble it, and then find instructions it executes when you return from a function, you can find out what the offset ought to be. For example, with system V calling conventions on x86, you'll see that it pops %ebp (the frame pointer) and then calls ret which pops the return address off the stack. So on entry into a function, it pushes the return address and frame pointer. Each push moves the stack pointer down by 4 bytes, so you want the stack pointer to start at the high address of the allocated region, -8 bytes (as if you just called a function to get there). We will fill the 8 bytes next.

The other thing you can do is write some very small (one line) inline assembly to manipulate the stack pointer, and then call setjmp. This is actually more portable, because in many systems the pointers in a jmpbuf are mangled for security, so you can't easily modify them.

I haven't tried it, but you might be able to avoid the asm by just deliberately overflowing the stack by declaring a very large array and thus moving the stack pointer.

Step 4
You need exiting threads to return the system to some safe state. If you don't do this, and one of the threads returns, it will take the address right above your allocated stack as a return address and jump to some garbage location and likely segfault. So first you need a safe place to return to. Get this by calling setjmp in the main thread and storing the jmpbuf in a globally accessible location. Define a function which takes no arguments and just calls longjmp with the saved global jmpbuf. Get the address of that function and copy it to your allocated stacks where you left room for the return address. You can leave the frame pointer empty. Now, when a thread returns, it will go to that function which calls longjmp, and jump right back into the main thread where you called setjmp, every time.

Step 5
Right after the main thread's setjmp, you want to have some code that determines which thread to jump to next, pulling the appropriate jmpbuf off the queue and calling longjmp to go there. When there are no threads left in that queue, the program is done.

Step 6
Write a context switch function which calls setjmp and stores the current state back on the queue, and then longjmp on another jmpbuf from the queue.

Conclusion
That's the basics. As long as threads keep calling context switch, the queue keeps getting repopulated, and different threads run. When a thread returns, if there are any left to run, one is chosen by the main thread, and if none are left, the process terminates. With relatively little code you can have a pretty basic cooperative multitasking setup. There are more things you probably want to do, like implement a cleanup function to free the stack of a dead thread, etc. You can also implement preemption using signals, but that is much more difficult because setjmp doesn't save the floating point register state or the flags registers, which are necessary when the program is interrupted asynchronously.

弥繁 2024-09-03 12:56:21

它可能稍微改变了规则,但 GNU pth 做到了这一点。这是可能的,但您可能不应该自己尝试,除非作为学术概念验证练习,如果您想认真地并以远程便携式方式进行操作,请使用 pth 实现 - 当您阅读时您就会明白为什么第 pth 线程创建代码。

(本质上,它使用信号处理程序来欺骗操作系统创建一个新的堆栈,然后 longjmp 离开那里并保留堆栈。它显然可以工作,但它很粗略。)

在生产代码中,如果您的操作系统支持 makecontext/交换上下文,请改用它们。如果它支持 CreateFiber/SwitchToFiber,请改用它们。并且要意识到一个令人失望的事实,即协程最引人注目的用途之一——即通过产生外部代码调用的事件处理程序来反转控制——是不安全的,因为调用模块必须是可重入的,而您通常可以不证明这一点。这就是 .NET 仍然不支持纤程的原因...

It may be bending the rules a little, but GNU pth does this. It's possible, but you probably shouldn't try it yourself except as an academic proof-of-concept exercise, use the pth implementation if you want to do it seriously and in a remotely portable fashion -- you'll understand why when you read the pth thread creation code.

(Essentially it uses a signal handler to trick the OS into creating a fresh stack, then longjmp's out of there and keeps the stack around. It works, evidently, but it's sketchy as hell.)

In production code, if your OS supports makecontext/swapcontext, use those instead. If it supports CreateFiber/SwitchToFiber, use those instead. And be aware of the disappointing truth that one of the most compelling use of coroutines -- that is, inverting control by yielding out of event handlers called by foreign code -- is unsafe because the calling module has to be reentrant, and you generally can't prove that. This is why fibers still aren't supported in .NET...

烟酉 2024-09-03 12:56:21

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

这是可能的,但容易出错,特别是如果您使用 setjmp 和 longjmp 的默认实现。这些函数的一个问题是,在许多操作系统中,它们只会保存 64 位寄存器的子集,而不是整个上下文。这通常是不够的,例如在处理系统库时(我的经验是使用 amd64/windows 的自定义实现,考虑到所有因素,它的工作相当稳定)。

也就是说,如果您不尝试使用复杂的外部代码库或事件处理程序,并且您知道自己在做什么,并且(特别是)如果您在汇编器中编写自己的版本来保存更多当前上下文(如果您使用 32 位 Windows 或 Linux,这可能没有必要,如果您使用某些版本的 BSD,我想它几乎肯定是必要的),并且您调试它并仔细注意反汇编输出,那么您可能能够实现什么你想要的。

This is a form of what is known as userspace context switching.

It's possible but error-prone, especially if you use the default implementation of setjmp and longjmp. One problem with these functions is that in many operating systems they'll only save a subset of 64-bit registers, rather than the entire context. This is often not enough, e.g. when dealing with system libraries (my experience here is with a custom implementation for amd64/windows, which worked pretty stable all things considered).

That said, if you're not trying to work with complex external codebases or event handlers, and you know what you're doing, and (especially) if you write your own version in assembler that saves more of the current context (if you're using 32-bit windows or linux this might not be necessary, if you use some versions of BSD I imagine it almost definitely is), and you debug it paying careful attention to the disassembly output, then you may be able to achieve what you want.

叹沉浮 2024-09-03 12:56:21

为了学习,我做了这样的事情。
https://github.com/Kraego/STM32L476_MiniOS/blob/ main/Usercode/Concurrency/scheduler.c

上下文/线程切换是通过setjmp/longjmp来完成的。困难的部分是正确分配堆栈(请参阅 allocateStack()),这取决于您的平台。

这只是一个演示如何工作,我永远不会在生产中使用它。

I did something like this for studies.
https://github.com/Kraego/STM32L476_MiniOS/blob/main/Usercode/Concurrency/scheduler.c

The context/thread switching is done by setjmp/longjmp. The difficult part was to get the allocated stack correct (see allocateStack()) this depends on your platform.

This is just a demonstration how this could work, I would never use this in production.

你如我软肋 2024-09-03 12:56:21

正如肖恩·奥格登已经提到的,
longjmp() 不适合多任务处理,因为
它只能向上移动堆栈,不能
在不同的堆栈之间跳转。不行。

正如 user414736 所提到的,您可以使用 getcontext/makecontext/swapcontext
功能,但问题在于
它们并不完全位于用户空间中。他们其实
调用 sigprocmask() 系统调用,因为它们会切换
信号掩码作为上下文切换的一部分。
这使得 swapcontext() 比 longjmp() 慢得多,
而且您可能不想要缓慢的协同例程。

据我所知,没有 POSIX 标准解决方案
这个问题,所以我从不同的地方编译了自己的
可用来源。你可以找到上下文操作
从 libtask 中提取的函数如下:
https://github.com/dosemu2/dosemu2/tree/开发/src/base/lib/mcontext

其功能是:
getmcontext()、setmcontext()、makemcontext() 和 swapmcontext()。
它们与具有相似名称的标准函数具有相似的语义,
但它们也模仿 getmcontext() 中的 setjmp() 语义
当通过 setmcontext() 跳转到时返回 1(而不是 0)。

除此之外,您可以使用协程库 libpcl 的端口:
https://github.com/dosemu2/dosemu2/tree/开发/src/base/lib/libpcl

这样,就可以实现快速协作的用户空间
线程。它可以在 Linux、i386 和 x86_64 架构上运行。

As was already mentioned by Sean Ogden,
longjmp() is not good for multitasking, as
it can only move the stack upward and can't
jump between different stacks. No go with that.

As mentioned by user414736, you can use getcontext/makecontext/swapcontext
functions, but the problem with those is that
they are not fully in user-space. They actually
call the sigprocmask() syscall because they switch
the signal mask as part of the context switching.
This makes swapcontext() much slower than longjmp(),
and you likely don't want the slow co-routines.

To my knowledge there is no POSIX-standard solution to
this problem, so I compiled my own from different
available sources. You can find the context-manipulating
functions extracted from libtask here:

https://github.com/dosemu2/dosemu2/tree/devel/src/base/lib/mcontext


The functions are:
getmcontext(), setmcontext(), makemcontext() and swapmcontext().
They have the similar semantic to the standard functions with similar names,
but they also mimic the setjmp() semantic in that getmcontext()
returns 1 (instead of 0) when jumped to by setmcontext().

On top of that you can use a port of libpcl, the coroutine library:

https://github.com/dosemu2/dosemu2/tree/devel/src/base/lib/libpcl


With this, it is possible to implement the fast cooperative user-space
threading. It works on linux, on i386 and x86_64 arches.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文