c++ setjmp和longjump以实现线程

7bsow1i6  于 2023-01-22  发布在  其他
关注(0)|答案(2)|浏览(213)

我有一个关于使用setjmp和longjump创建可以彼此独立运行的函数堆栈的问题。
这里,B()的函数堆栈似乎在A的函数堆栈之上,所以当A超出作用域,我尝试长跳转到B()时,代码segfaults。

#include <stdio.h>
#include <setjmp.h>
#include <iostream>
using namespace std;

jmp_buf bufferA, bufferB;

void routineB(); // forward declaration 

void routineA()
{
    int r ;

    printf("(A1)\n");

    r = setjmp(bufferA);
    if (r == 0) routineB();

    printf("(A2) r=%d\n",r);

    r = setjmp(bufferA);
    if (r == 0) longjmp(bufferB, 20001);

    printf("(A3) r=%d\n",r);

    r = setjmp(bufferA);
    if (r == 0) longjmp(bufferB, 20002);

    printf("(A4) r=%d\n",r);
}

void routineB()
{
    int r;

    printf("(B1)\n");

    r = setjmp(bufferB);
    if (r == 0) longjmp(bufferA, 10001);

    printf("(B2) r=%d\n", r);

    r = setjmp(bufferB);
    if (r == 0) longjmp(bufferA, 10002);

    printf("(B3) r=%d\n", r);

    r = setjmp(bufferB);
    if (r == 0) longjmp(bufferA, 10003);

    cout << "WHAT" << endl;
}

int main(int argc, char **argv) 
{
    routineA();
    longjmp(bufferB, 123123);
    return 0;
}

我在想,我们可以让每个协程(在这个例子中是B和A)跳回到一个主函数,然后再跳到一个协程,如果它们是活的。这会起作用吗?或者这也会导致segfault,因为一些可能死的协程的堆栈在想要运行的协程的堆栈之上?
如果它看起来确实应该segfault,那么如何在C++中实现这样独立的coutroutines(当其他coutroutines已经死亡时,它们可以运行,并且不依赖于彼此)?
注意:我不想使用swapcontext、makecontext、setcontext和getcontext,因为它们已经过时了。这篇文章的目的是帮助我找到这些函数的替代方法
先谢了!

kb5ga3dv

kb5ga3dv1#

它应该能起作用,但需要一些预防措施。它在90年代曾经起作用。
只要栈中较旧的线程不返回,它就可以工作,那么就不会有问题。对于生成器或线程,这意味着它以某个最终产出终止,但不返回。
我记得我曾要求IBM修复他们的AIX实现setjmp/longjmp,以便它停止检查(和禁止)其他实现允许的有用行为,这是我当时发现的实现协作线程的最可移植的方法。
使用基于状态机的堆栈分配器可以做得更好,以便将不同大小的堆栈分配/释放给不同的线程。
这是非常棘手的,但据我所知,我记得Matz(来自Ruby)问我几年后的代码,但唉,我没有更多的访问它的时间。
无论如何,这在今天并不重要,因为pthread完成了这项工作。

5anewei6

5anewei62#

快速和肮脏的黑客在Windows上。抱歉混乱的命名和混乱的代码。

// mythreads.cpp : Defines the entry point for the console application.
//

#include <setjmp.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include "List.h"

#include <Windows.h>

struct thread_t 
{
  util::Node node;
  jmp_buf buf;
};

util::List thread_list;
thread_t* runningThread = NULL;

void schedule(void);

void thread_yield()
{
  if (setjmp(runningThread->buf) == 0)
  {
    thread_list.push_back(&runningThread->node);
    schedule();
  }
  else
  {
    /* EMPTY */
  }
}

void myThread1(void *args)
{
  printf("myThread1\n");
}

void startThread(void(*func)(void*), void *args)
{
  thread_t* newThread = (thread_t*)malloc(sizeof(thread_t));

  // Create new stack here! This part is Windows specific. Find the current stack
  // and copy the contents. One might do more stuff here, e.g. change the return address 
  // of this function to be a thread_exit function.
  NT_TIB* tib = (NT_TIB*)__readfsdword(0x18);
  uint8_t* stackBottom = (uint8_t*)tib->StackLimit;
  uint8_t* stackTop = (uint8_t*)tib->StackBase;

  size_t stack_size = stackTop - stackBottom;
  uint8_t *new_stack = (uint8_t*)malloc(stackTop - stackBottom);
  memcpy(new_stack, stackBottom, stack_size);

  _JUMP_BUFFER *jp_buf = (_JUMP_BUFFER*)newThread->buf;

  if (setjmp(newThread->buf) == 0)
  {
    // Modify necessary registers to point to new stack. I may have 
    // forgotten a bunch of registers here, you must do your own homework on
    // which registers to copy.
    size_t sp_offset = jp_buf->Esp - (unsigned long)stackBottom;
    jp_buf->Esp = (size_t)new_stack + sp_offset;
    size_t si_offset = jp_buf->Esi - (unsigned long)stackBottom;
    jp_buf->Esi = (size_t)new_stack + si_offset;
    size_t bp_offset = jp_buf->Ebp - (unsigned long)stackBottom;
    jp_buf->Ebp = (size_t)new_stack + bp_offset;
    thread_list.push_back(&newThread->node);
  }
  else
  {
    /* This is where the new thread will start to execute */
    func(args);
  }
}

void schedule(void)
{
  if (runningThread != NULL)
  {

  }

  if (thread_list.size() > 0)
  {
    thread_t* t = (thread_t*)thread_list.pop_front();
    runningThread = t;
    longjmp(t->buf, 1);
  }
}

void initThreading()
{
  thread_list.init();

  thread_t* mainThread = (thread_t*)malloc(sizeof(thread_t));

  if (setjmp(mainThread->buf) == 0)
  {
    thread_list.push_back(&mainThread->node);
    schedule();
  }
  else
  {
    /* This is where the main thread will start to execute again */
    printf("Main thread running!\n");
  }
}

int main()
{
  initThreading();

  startThread(myThread1, NULL);

  thread_yield();

  printf("Main thread exiting!\n");
}

此外,微软和setjmp/longjmp可能不是一个很好的匹配。如果我附近有一个Unix机器,我会在那个机器上做。
而且我可能还得多研究一下堆栈复制,以确保它能正确工作。
编辑:要检查堆栈更改后要修改哪些寄存器,可以查阅编译器手册.
代码从main开始()通过设置线程框架来实现。它通过处理主线程来实现(此时唯一的线程)作为其他线程中的一个线程。因此,存储主线程的上下文并将其放在thread_list中。然后我们调度()来让一个线程运行。这是在initThreading()中完成的。因为唯一运行的线程是主线程,所以我们将在main()函数中继续。
main函数要做的下一件事是将函数指针作为参数的startThread(以及要发送到func的arg NULL)。()函数的作用是为新线程的堆栈分配内存(每个线程都需要自己的堆栈)。分配之后,我们将运行线程的上下文保存到新线程上下文缓冲区中(jmp_buf)并更改堆栈指针(以及所有其他应该被改变的寄存器),使它指向新创建的堆栈。然后我们将这个新线程添加到等待线程列表中。然后我们从函数返回。我们仍然在主线程中。
在主线程中,我们执行thread_yield(),它会说“好吧,我不想再运行了,让别人运行吧!"thread_yield函数存储当前上下文,并将主线程放在thread_list的后面,然后我们进行调度。
Schedule()从thread_list中获取下一个线程,并对保存在线程buf(jmp_buf)中的上下文执行longjmp操作,在这种情况下,线程将继续在setjmp的else子句中运行,我们在这里存储了上下文。

else
    {
        /* This is where the new thread will start to execute */
        func(args);
      }

它将一直运行,直到我们执行thread_yield,然后我们将执行相同的操作,但取而代之的是主线程并对其保存的缓冲区执行longjmp,等等...
如果您想做得更好并且想要时间片,可以实现一些警报来保存当前线程上下文,然后调用schedule()。
明白了吗?

相关问题