How to implement fiber

本文深入探讨了纤程(Fiber)的工作原理和技术细节,包括纤程与线程的区别、纤程的调度机制以及如何在Win32和Linux环境下实现纤程的创建与切换。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Introduction

我们知道线程是程序执行的一个最基本的单位,任何程序的执行,都依赖于线程的执行。而线程通常是操作系统的基本组成,通常创建一个线程,比如在Win32上,用CreateThread创建一个线程,操作系统实际会创建2个对象,一个是用户态的线程,另一个是内核态的线程,而我们的代码运行在用户态线程。当线程切换时,比如调用WaitForSingleObject,或者调用WriteFile执行阻塞IO时,通常会涉及到线程的切换,而切换需要进入到内核态,由此带来的开销是比较可观的。而Fiber,也就是纤程,完全运行在用户态,各个线程的切换也只在用户态完成,所以切换开销较小。线程的调度,通常是由操作系统的线程调度器完成,在现代OS中,通常使用抢占式调度策略。而纤程的调用,完全依赖于程序员自己,即实现一种合作式调度,只有在主动提出切换时,才会进行切换。

纤程,在其他的语言,比如Python、Lua中都有实现。Python中提供了generate,可以用来简单地模拟纤程,而另外一个强大的greenlet库,则是Python中纤程的另一实现。在Lua中,我们可以使用内置的coroutine库。

Basic Usage

 
Win32

在Win32中,我们可以使用ConvertThreadToFiber/ConvertFiberToThread,CreateFiber/DeleteFiber来管理纤程的创建与销毁。对于一个线程,如果要调用纤程,那么必须调用Convert,将一个线程转化为纤程。当需要切换到另一个纤程时,只需要调用SwitchToFiber,这样现有的线程的一些状态将被保存,等待以后恢复执行。

Linux

Linux中有context api,可以用来完成类似的功能。我们调用getcontext/makecontext,来初始化纤程,用swapcontext/setcontext来切换纤程。

setjmp/longjmp

说到这里,我们不得不提起另一个看似可以用来完成类似工作的古老的C API,setjmp/longjmp。setjmp用来保存当前的执行环境,longjmp用来还原上次的执行,这样可以实现non-local goto的功能。但是这里有一个问题,就是当我们调用longjmp回到setjmp保存的状态继续执行时,如果longjmp的调用者与setjmp的调用者不是同一个函数,那么longjmp所在的栈的状态将是undefined[1]。也就是说,我们不能通过在longjmp之前,save状态,之后再longjmp回来,这是未定义的行为。当然,在win32上,你可以这么做,而且还工作地很好(我在实现中使用了这一技巧)。但是在linux上,你将会遇到运行时错误,提示stack smashing(gcc的stack保护机制,__fortify_fail),也就是栈被破坏了(这令我debug了很长时间,最后放弃,网上看到许多实现用这方法,但是不奏效)。setjmp/longjmp还有其他陷阱,比如,在win32上,他不会保存/恢复SEH异常链,等等。

How Fiber works

其实无论是哪种方法,我们只需要明白Fiber是如果工作的,那么就可以实现出自己的fiber来(当然这里还需要考虑其他一些CPU相关的情况)。

Fibe类似于线程,都有一个栈用来保存当前的调用所需的状态。所以我们首先需要为fiber创建一个栈。其次由于每个fiber肯定需要一个入口函数(就像线程一样),在切换时,需要进入到这个入口,然后执行。其实代码的执行在x86 CPU上,就是修改EIP指针,将其指向这个入口函数即可。在切换纤程时,也就是保存我们的栈的状态,x86上,ESP和EBP是两个重要的寄存器,保存了当前的栈的状态。我们还需要保存其他的通用寄存器,EBX、EDI、ESI,因为不同纤程显然会修改寄存器。这里不保存其他3个寄存器:EAX、ECX、EDX的原因是,这些寄存器都是caller-save的[4],也就是说,如果调用者使用了这些寄存器,那么在调用其他函数前,必须先保存这些寄存器。

这里涉及到两个问题,一个是修改EIP指针,另一个是保存/恢复寄存器值。

Modify EIP

因为EIP指针只有在特权模式才能够修改(操作系统工作在特权模式),我们用户态程序是无法直接修改的。但是我们知道,jmp指令时可以间接修改EIP的。还有另一个方法是用ret指令,ret指令会从栈顶取出值作为EIP的值,这样就实现了跳转。这里我选择使用push + ret的方式来修改eip,因为jmp要求使用相对于下一条指令的偏移作为操作数(Relative jumpping)。

Save/Restore GPR(general-purpose registers)

保存和回复GPR比较简单,用几条汇编指令即可完成。

Save/Restore Stack Pointer

在Save/Restore EBP/ESP时,需要格外小心,因为一旦我们修改了ESP,那么在之后所有的对栈的操作都将在新的栈上进行。

Implementation

先来看一下fiber_context的成员,这个struct用来保存寄存器和栈的指针/大小等。

struct fiber_context
{
#ifdef FIBER_X86
    // registers
    uintptr_t ebp;
    uintptr_t esp;
    uintptr_t eip;
    // callee-save general-purpose registers
    // see http://www.agner.org/optimize/calling_conventions.pdf
    uintptr_t ebx;
    uintptr_t esi;
    uintptr_t edi;
#else
#  error Unsupported platform
#endif

    char* stack;
    int   stack_size;
    void* userarg;
};
 

所有callee-save的GPR,以及ebp/esp/eip都会被保存。这个比较简单。

fiber_make_context

这个函数用来创建一个context,初始化执行环境。

void fiber_make_context(fiber_context* context, fiber_entry entry, void* arg)
{
    assert(context && entry);
    // default alignment of the stack
    const int alignment = 16;

    context->esp = (reinterpret_cast<uintptr_t>(context->stack) + context->stack_size) & ~(alignment - 1);
    context->ebp = context->esp;
    context->eip = reinterpret_cast<uintptr_t>(entry);
    context->userarg = arg;

    // push the argument onto the stack
    char* top = reinterpret_cast<char*>(context->esp);
    memcpy(top, &context->userarg, sizeof(void*));
    // make space for the pushed argument
    context->esp -= sizeof(void*);

    // clear all callee-save general purpose registers
    context->ebx = context->esi = context->edi = 0;
}
这里,我们初始化了esp,和ebp,同时指向栈顶。注意,x86 CPU使用full-descending stack,也就是逆向增长的栈。因为用户可以提供一个可选参数,所以我们必须事先将这个参数压栈,这样在将EIP重定向后,入口函数就可以正常存取这个参数了。自然,压栈后,我们必须减小esp的值,为参数留出空间。EIP的值指向入口函数的地址,这个很容易理解。其余部分只是简单地初始化GPR的默认值。

fiber_get_context

该函数用来保存当前的执行状态。因为我们可以通过fiber_set_context来从一个由fiber_get_context初始化的context中恢复执行,这里必须考虑这种特殊情况。我们不能简单地保存esp/ebp的值,因为一旦我们从fiber_get_context返回,该函数的stack frame将会被销毁,这样如果保存的esp/ebp指向的是fiber_get_context的frame的话,那么很显然会出现运行时错误。我们唯一能够做的就是,保存调用者的栈。

对于每一个c/c++函数,编译器都会在入口处安插指令来保存调用者的ebp,并且修改ebp/esp来创建新的stack frame。所以我们不能写一个普通函数来完成这个工作。我们需要一种方法,不让编译器生成prolog/epilog,这样我们就有更多的控制权。在VC中,我们可以使用naked函数,在GCC,我们只能写汇编源代码。

__declspec(naked) void fiber_get_context(fiber_context* context)
{
    // TODO: how much space need to reserve for assert ?
    //assert(context);
    // save the current context in `context' and return
    __asm
    {
        // save current stack pointer to context
        mov ecx, dword ptr [esp + 0x4] ;
        fixup, point to the argument, ignore return address
        mov dword ptr [ecx], ebp ; context->ebp
        mov eax, esp
        // fixup esp, ignore return address, as the eip is set to the caller's address
        add eax, 0x4
        mov dword ptr [ecx + 0x4], eax ; context->esp
        mov eax, dword ptr [esp]
        mov dword ptr [ecx + 0x8], eax ; context->eip
        // save callee-save general-purpose registers
        mov dword ptr [ecx + 0xc],  ebx; context->ebx
        mov dword ptr [ecx + 0x10], esi; context->esi
        mov dword ptr [ecx + 0x14], edi; context->edi
        ret
    }
}
这里在保存调用者的esp时,我进行了一些修正,以得到调用fiber_get_context之间的值(这个值包括被压栈的参数)。在x86上,当调用一个函数时,调用者与被调用者栈的布局是这样的


所以,要拿到返回值,只需要将ebp加上4即可。但是由于我们是一个naked function,所以,这里我们只能通过esp来取值,考虑压栈的eax, ebx,对esp加上8,即可得到返回地址。

fiber_set_context

该函数用于切换到另一个context,调用该函数后,会直接切换到新的fiber,而控制流不会返回,所以我们可以不用考虑栈的使用情况,而只需简单回复即可。 

void fiber_set_context(fiber_context* context)
{
    __asm
    {
        ; restore the enviroment for context
        mov eax, context
        mov ebp, dword ptr [eax]        ; context->ebp
        mov esp, dword ptr [eax + 0x4]  ; context->esp
        ; restore callee-save general-purpose registers
        mov ebx, dword ptr [eax + 0xc]  ; context->ebx
        mov edx, dword ptr [eax + 0x10] ; context->edx
        mov esi, dword ptr [eax + 0x14] ; context->esi
        mov edi, dword ptr [eax + 0x18] ; context->edi
        push dword ptr [eax + 0x8]      ; context->eip
        ret
        ; should never return here
    }
}
这个函数看上去比较简单,只是简单地恢复寄存器的值,并设置eip指针。
fiber_swap_context

这个函数会保存当前的context,并切换到新的context。这个函数可以用fiber_get_context/fiber_set_context来实现。

void fiber_swap_context(fiber_context* oldcontext, fiber_context* newcontext)
{
    assert(oldcontext && newcontext);
    // save the current context in the oldcontext and set the current context from newcontext
    __asm
    {
        push oldcontext
        call fiber_get_context
        // fixup oldcontext->esp, ignore pushed arguments, since we'll resume from restore
        mov eax, oldcontext
        add dword ptr[eax + 0x4], 0x4
        // fixup return address
        mov dword ptr[eax + 0x8], offset restore
        // switch to newcontext
        push newcontext
        call fiber_set_context
        restore:
    }
}
在保存当前执行环境时需要注意一点,因为我们调用fiber_get_context时save的eip指针的值应该是call的下一条指令的地址,在这里就是push eax,但是我们不希望这样,因为随后我们就会调用fiber_set_context。所以我们必须修正EIP,以跳过对fiber_set_context的调用。这里还有一点需要注意,那就是esp的值,由于我们调用fiber_get_context手动对参数进行压栈,所以在从restore恢复执行时,esp的值还包含oldcontext这个参数。但是restore之后我们就直接返回了,所以我们必须修正esp的值,减去压栈的oldcontext(其实这里理论上不用修正esp,因为在函数的epilog中,会自动将ebp赋值给esp,所以修正没有意义,但是由于VC会在函数最后安插栈完整性的检查代码,所以为了防止这个错误,必须修正)。

TODO

这里我们只保存了GPRs,没有对其他的寄存器,比如浮点控制寄存器等进行保存。

Summary

在无法大量创建线程的环境中,纤程提供了一定的解决方案,因为纤程更加轻量,从而可以实现更高的并发性。

Source Code

代码存放在github上便于下载,git url为git://github.com/alexshen/fiber.git,网页地址为https://github.com/alexshen/fiber

Reference

[1] setjmp.h Wikipeida
[2] qemu coroutine
[3] Gcc Inline Assembly
[4]Calling conventions for different C++ compilers and operating systems

### Aniso Shape Model in Computer Vision and Graphics Aniso shape models represent a class of techniques that allow for the manipulation and analysis of shapes with anisotropic properties, meaning different material or deformation characteristics along various directions. In computer vision and graphics, these models are particularly useful when dealing with objects whose behavior is direction-dependent. In free-form deformations (FFD), one approach to manipulating complex geometries involves using control lattices around solid geometric models[^1]. However, traditional FFD methods assume isotropy within elements of this lattice. For more sophisticated applications requiring directional sensitivity, such as simulating muscle movements under skin or analyzing fiber orientations in composite materials, an extension towards anisotropy becomes necessary. To implement an aniso shape model effectively: #### Defining Material Properties Material parameters must be defined locally at each point on the surface or volume being modeled. These can include stiffness tensors which capture how forces applied from certain angles result in specific displacements differently than others would cause. ```python import numpy as np def define_material_properties(stiffness_tensor): """ Defines local material property through stiffness tensor. Args: stiffness_tensor (np.ndarray): A 3x3 matrix representing elastic moduli Returns: dict: Dictionary containing material information """ return {"stiffness": stiffness_tensor} ``` #### Incorporating Directional Sensitivity Directionality should influence both rendering outcomes and physical simulations by adjusting texture mapping based upon orientation relative to view/camera position while also affecting collision detection/response algorithms during animation sequences. For instance, consider implementing adaptive sampling strategies where samples taken perpendicularly versus parallelly across surfaces yield distinct visual textures due to underlying microstructure variations captured via high-resolution scans or procedural generation routines. #### Applications in Medical Imaging One notable application area lies within medical imaging processing pipelines aiming to reconstruct three-dimensional representations of organs like hearts or brains accurately. Herein, capturing regional differences between tissues types—such as white matter vs gray matter—is crucial since they exhibit markedly dissimilar mechanical behaviors under stress conditions.
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值