st(state-threads)coroutine와setjmp/longjmp의 관계

14194 단어
st(state-threads) https://github.com/winlinvip/state-threads
st 기반 RTMP/HLS 서버:https://github.com/winlinvip/simple-rtmp-server
st는 coroutine의 메커니즘, 즉 사용자 상태 스레드 또는 협동 스레드를 실현했다.epoll(async,nonblocking socket)의 비저지를 협동 방식으로 바꾸어 모든 상태 공간을 stack에 넣고 비동기적인 대순환과 상태 공간의 판단을 피한다.
st에 대한 자세한 내용은 번역을 참조하십시오.http://blog.csdn.net/win_lin/article/details/8242653
본고는 주로 coroutine가 setjmp와longjmp를 바탕으로 하는 실현 메커니즘을 소개했다.
나는 st를 간소화하여 다른 시스템을 없애고 linux 시스템만 고려하며 i386/x86_64/arm/mips 네 가지 cpu 시리즈, 참고:https://github.com/winlinvip/simple-rtmp-server/tree/master/trunk/research/st
st의 가장 관건적인 점은 stack을 재분배해야 한다는 것이다. 예를 들어 heap에서 stack을 분배하면 초대합병을 지원한다.
mips와arm는stack를 직접 설정할 수 있습니다.반면 i386 및 x86_64 CPU 시스템이glibc2.4 이상이므로 보안을 위해 jmp_buf의 구조가 그리 명확하지 않습니다. jmp_buf의 sp는 실행할 수 없습니다.
    /*
     * Starting with glibc 2.4, JB_SP definitions are not public anymore.
     * They, however, can still be found in glibc source tree in
     * architecture-specific "jmpbuf-offsets.h" files.
     * Most importantly, the content of jmp_buf is mangled by setjmp to make
     * it completely opaque (the mangling can be disabled by setting the
     * LD_POINTER_GUARD environment variable before application execution).
     * Therefore we will use built-in _st_md_cxt_save/_st_md_cxt_restore
     * functions as a setjmp/longjmp replacement wherever they are available
     * unless USE_LIBC_SETJMP is defined.
     */

glic의 코드를 참고할 수 있습니다.http://ftp.gnu.org/gnu/glibc/

MIPS


이런 게 제일 쉬워요. 실제로 setjmp의 jmp_buf는 sp와 pc를 제공합니다. jmp_만buf의 sp는 분배된 stack으로 설정하고 pc를main 주소로 설정하면 됩니다.
#if defined(__mips__)
    #define MD_STACK_GROWS_DOWN
    
    #define MD_INIT_CONTEXT(_thread, _sp, _main)               \
        ST_BEGIN_MACRO                                           \
        MD_SETJMP((_thread)->context);                           \
        _thread->context[0].__jmpbuf[0].__pc = (__ptr_t) _main;  \
        _thread->context[0].__jmpbuf[0].__sp = _sp;              \
        ST_END_MACRO

MIPS의 libc 헤더 파일 정의 jmp_buf 때 pc와 sp를 지정했습니다.

ARM


ARM은 실제로 st도 사용하는glibc의setjmp와longjmp,뚜렷한arm의glibc의jmp_buf는 구조를 알 수 있습니다. arm의 setjmp 헤더 파일을 참고하십시오.
    /**
        /usr/arm-linux-gnueabi/include/bits/setjmp.h
        #ifndef _ASM
        The exact set of registers saved may depend on the particular core
           in use, as some coprocessor registers may need to be saved.  The C
           Library ABI requires that the buffer be 8-byte aligned, and
           recommends that the buffer contain 64 words.  The first 28 words
           are occupied by v1-v6, sl, fp, sp, pc, d8-d15, and fpscr.  (Note
           that d8-15 require 17 words, due to the use of fstmx.)
        typedef int __jmp_buf[64] __attribute__((__aligned__ (8)));
        
        the layout of setjmp for arm:
            0-5: v1-v6 
            6: sl
            7: fp
            8: sp
            9: pc
            10-26: d8-d15 17words
            27: fpscr
    */
    /**
    For example, on raspberry-pi, armv6 cpu:
        (gdb) x /64 env_func1[0].__jmpbuf
            v1, 0:  0x00	0x00	0x00	0x00	
            v2, 1:  0x00	0x00	0x00	0x00
            v3, 2:  0x2c	0x84	0x00	0x00	
            v4, 3:  0x00	0x00	0x00	0x00
            v5, 4:  0x00	0x00	0x00	0x00	
            v6, 5:  0x00	0x00	0x00	0x00
            sl, 6:  0x00	0xf0	0xff	0xb6	
            fp, 7:  0x9c	0xfb	0xff	0xbe
            sp, 8:  0x88	0xfb	0xff	0xbe	
            pc, 9:  0x08	0x85	0x00	0x00
        (gdb) p /x $sp
        $5 = 0xbefffb88
        (gdb) p /x $pc
        $4 = 0x850c
    */

디버깅 결과도 문제가 없는 것으로 나타났다.st JB_ 수정이 필요합니다.RSP, 20에서 8로 변경, 참조:https://github.com/winlinvip/simple-rtmp-server/issues/190

i386/x86_64


st는 i386 및 x86_64 매크로 MD_ 정의USE_BUILTIN_SETJMP, 즉 st 자신의 md.S에 있는 setjmp와 longjmp:
    /*
     * Starting with glibc 2.4, JB_SP definitions are not public anymore.
     * They, however, can still be found in glibc source tree in
     * architecture-specific "jmpbuf-offsets.h" files.
     * Most importantly, the content of jmp_buf is mangled by setjmp to make
     * it completely opaque (the mangling can be disabled by setting the
     * LD_POINTER_GUARD environment variable before application execution).
     * Therefore we will use built-in _st_md_cxt_save/_st_md_cxt_restore
     * functions as a setjmp/longjmp replacement wherever they are available
     * unless USE_LIBC_SETJMP is defined.
     */
    #if defined(__i386__)
        #define MD_STACK_GROWS_DOWN
        #define MD_USE_BUILTIN_SETJMP
        
        #if defined(__GLIBC__) && __GLIBC__ >= 2
            #ifndef JB_SP
                #define JB_SP 4
            #endif
            #define MD_GET_SP(_t) (_t)->context[0].__jmpbuf[JB_SP]
        #else
            /* not an error but certainly cause for caution */
            #error "Untested use of old glibc on i386"
            #define MD_GET_SP(_t) (_t)->context[0].__jmpbuf[0].__sp
        #endif
    #elif defined(__amd64__) || defined(__x86_64__)
        #define MD_STACK_GROWS_DOWN
        #define MD_USE_BUILTIN_SETJMP
        
        #ifndef JB_RSP
            #define JB_RSP 6
        #endif
        #define MD_GET_SP(_t) (_t)->context[0].__jmpbuf[JB_RSP]

원인은 명백히 말하는데,glibc2.4 이상의 jmp_buf의 sp를 조작할 수 없어서 st에 설치된 setjmp와longjmp만 사용할 수 있습니다.
glibc의 setjmp와 longjmp를 사용하면 매크로 정의(다음 장 참조: ST 매크로 정의) USE_LIBC_SETJMP, segmentfault, gdb 디버깅setjmp의 jmp_buf:
        (gdb) x /64xb env_func1[0].__jmpbuf
        0x600ca0 <env_func1>:     0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
        0x600ca8 <env_func1+8>:   0xf8	0xc1	0x71	0xe5	0xa8	0x88	0xb4	0x15
        0x600cb0 <env_func1+16>:  0xa0	0x05	0x40	0x00	0x00	0x00	0x00	0x00
        0x600cb8 <env_func1+24>:  0x90	0xe4	0xff	0xff	0xff	0x7f	0x00	0x00
        0x600cc0 <env_func1+32>:  0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
        0x600cc8 <env_func1+40>:  0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
        0x600cd0 <env_func1+48>:  0xf8	0xc1	0x51	0xe5	0xa8	0x88	0xb4	0x15
        0x600cd8 <env_func1+56>:  0xf8	0xc1	0xd9	0x2f	0xd7	0x77	0x4b	0xea
        (gdb) p /x $sp
        $4 = 0x7fffffffe380

분명히 볼 수 있어, jmp_buf에 sp와 대응할 수 있는 데이터가 하나도 없기 때문에 st는 setjmp 다음에 sp를 설정하면 문제가 있습니다.

ST 매크로 정의


s 매크로 정의, EXTRA_ 지정CFLAGS 매개변수는 다음을 참조하십시오.
##########################
# Other possible defines:
# To use poll(2) instead of select(2) for events checking:
# DEFINES += -DUSE_POLL
# You may prefer to use select for applications that have many threads
# using one file descriptor, and poll for applications that have many
# different file descriptors.  With USE_POLL poll() is called with at
# least one pollfd per I/O-blocked thread, so 1000 threads sharing one
# descriptor will poll 1000 identical pollfds and select would be more
# efficient.  But if the threads all use different descriptors poll()
# may be better depending on your operating system's implementation of
# poll and select.  Really, it's up to you.  Oh, and on some platforms
# poll() fails with more than a few dozen descriptors.
#
# Some platforms allow to define FD_SETSIZE (if select() is used), e.g.:
# DEFINES += -DFD_SETSIZE=4096
#
# To use malloc(3) instead of mmap(2) for stack allocation:
# DEFINES += -DMALLOC_STACK
#
# To provision more than the default 16 thread-specific-data keys
# (but not too many!):
# DEFINES += -DST_KEYS_MAX=<n>
#
# To start with more than the default 64 initial pollfd slots
# (but the table grows dynamically anyway):
# DEFINES += -DST_MIN_POLLFDS_SIZE=<n>
#
# Note that you can also add these defines by specifying them as
# make/gmake arguments (without editing this Makefile). For example:
#
# make EXTRA_CFLAGS=-DUSE_POLL <target>
#
# (replace make with gmake if needed).
#
# You can also modify the default selection of an alternative event
# notification mechanism. E.g., to enable kqueue(2) support (if it's not
# enabled by default):
#
# gmake EXTRA_CFLAGS=-DMD_HAVE_KQUEUE <target>
#
# or to disable default epoll(4) support:
#
# make EXTRA_CFLAGS=-UMD_HAVE_EPOLL <target>
#
##########################

예를 들어 st는 기본적으로 mmap 분배 창고를 사용하는데 성능을 향상시키기 위해 st를 heap 분배 창고에 두면 수백만 개의 라인을 지원할 수 있을 것으로 추정된다.컴파일할 때 매크로 MALLOC_ 정의STACK:
make linux-debug EXTRA_CFLAGS="-DMALLOC_STACK"

또는 Makefile에서 기본 DEFINES를 변경해도 됩니다.

SETJMP 및 LONGJMP


st의 스레드 스케줄링과 생명 주기를 보고 나서야 나는 setjmp와longjmp의 창고 전환 방식, 그리고 st가 자신이 분배한stack을 사용하여 도대체 어떻게 전환하는지 완전히 알게 되었다.참조:http://blog.csdn.net/win_lin/article/details/40978665
단일 루틴 프로그램을 고려하면 실제 프로그램은 유수선에서 실행된다. 즉main부터 실행하고 각 피드 함수에 들어가서 종료한다.참조:https://github.com/winlinvip/simple-rtmp-server/blob/master/trunk/research/arm/jmp_flow.cpp
/*
# for all supports setjmp and longjmp:
    g++ -g -O0 -o jmp_flow jmp_flow.cpp
*/
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>

jmp_buf context_level_0;

void func_level_0()
{
    const char* level_0_0 = "stack variables for func_level_0";
    int ret = setjmp(context_level_0);
    printf("func_level_0 ret=%d
", ret); if (ret != 0) { printf("call by longjmp.
"); exit(0); } } int main(int argc, char** argv) { func_level_0(); longjmp(context_level_0, 1); return 0; }

이 프로그램을 디버깅하려면 다음과 같이 하십시오.
(gdb) f 0
#0  func_level_0 () at jmp_flow.cpp:16
16	    if (ret != 0) {
(gdb) bt
#0  func_level_0 () at jmp_flow.cpp:16
#1  0x0000000000400725 in main (argc=1, argv=0x7fffffffe4b8) at jmp_flow.cpp:24
(gdb) i locals
level_0_0 = 0x400838 "stack variables for func_level_0"
ret = 0

setjmp 이후에stack은 유효하고 상부의 변수 값도 맞습니다.그러나
setjmp는 각종 바늘을 저장했을 뿐, 완전한stack의 복사본은 저장하지 않았습니다.따라서 setjmp가 나중에 돌아오면longjmp가 돌아올 때 전체 스택이 파괴됩니다.
(gdb) f 0
#0  func_level_0 () at jmp_flow.cpp:16
16	    if (ret != 0) {
(gdb) bt
#0  func_level_0 () at jmp_flow.cpp:16
#1  0x0000000000400734 in main (argc=1, argv=0x7fffffffe4b8) at jmp_flow.cpp:25
(gdb) i locals
level_0_0 = 0x1 <error: Cannot access memory at address 0x1>
ret = 1

이 함수가 되돌아온 후에 창고가 방출되었기 때문에 다시 이곳으로 뛰어들어 실행합니다. 실행 위치(PC)가 맞고, 창고 바늘도 맞습니다. 하지만 창고의 내용은 틀림없이 다를 것입니다.
따라서longjmp가 어느 곳에 도착했을 때 이 함수의 창고는 사실상 무효이고 변수에 접근하고 주소를 되돌려주는 것도 사용할 수 없습니다. 따라서longjmp는 계속longjmp만 할 수 있습니다. 이것이 바로 왜_가st_thread_main의 원인은 이 함수에서 영원히 돌아오지 않습니다.
또는longjmp가 어떤 함수에 이르면 하위 함수를 호출할 수 있으나,longjmp를 통해서만 이 함수 이외의 함수로 돌아갈 수 있다.또는, 첫 번째longjmp의 함수 (즉 함수의thread_main) 는 영원히 돌아올 수 없고, longjmp를 통해서만 돌릴 수 있다.
또는longjmp의 목표는 같은 stack일 뿐, sp를 바꾸지 않는 상황에서.st 그러면 이리저리 뛰어다니는 방식이 필요합니다. 반드시 더미 위에 sp를 분배하여 모든 라인을 자신의 sp로 만들어야 합니다.

stack을 할당하지 않는 세그먼트 오류


longjmp 이후에 돌아올 수 없습니다. 만약에 다시 longjmp가 다른 라인으로 가면 창고는 공용입니다. 이때 창고가 혼동될 수 있습니다.
코드 보기, 참조:https://github.com/winlinvip/simple-rtmp-server/blob/master/trunk/research/arm/jmp_2flow.cpp
/*
# for all supports setjmp and longjmp:
    g++ -g -O0 -o jmp_2flow jmp_2flow.cpp
*/
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>

jmp_buf context_thread_0;
jmp_buf context_thread_1;

void thread0_functions()
{
    int ret = setjmp(context_thread_0);
    // when ret is 0, create thread,
    // when ret is not 0, longjmp to this thread.
    if (ret == 0) {
        return;
    }
    
    int age = 10000;
    const char* name = "winlin";
    printf("[thread0] age=%d, name=%s
", age, name); if (!setjmp(context_thread_0)) { printf("[thread0] switch to thread1
"); longjmp(context_thread_1, 1); } // crash, for the stack is modified by thread1. // name = 0x2b67004009c8 <error: Cannot access memory at address 0x2b67004009c8> printf("[thread0] terminated, age=%d, name=%s
", age, name); exit(0); } void thread1_functions() { int ret = setjmp(context_thread_1); // when ret is 0, create thread, // when ret is not 0, longjmp to this thread. if (ret == 0) { return; } int age = 11111; printf("[thread1] age=%d
", age); if (!setjmp(context_thread_1)) { printf("[thread1] switch to thread0
"); longjmp(context_thread_0, 1); } printf("[thread1] terminated, age=%d
", age); exit(0); } int main(int argc, char** argv) { thread0_functions(); thread1_functions(); // kickstart longjmp(context_thread_0, 1); return 0; }

이 두 함수는longjmp가 성공했지만 stack가 서로 헷갈려서 세그먼트 오류가 발생했습니다.
Breakpoint 1, thread0_functions () at jmp_2flow.cpp:23
23	    printf("[thread0] age=%d, name=%s
", age, name); (gdb) i locals ret = 1 age = 10000 name = 0x400908 "winlin"

thread0에 처음 들어갔을 때 창고는 ok였습니다.그리고thread1로 뛰어들어 파괴합니다.마지막으로thread0:
Breakpoint 2, thread0_functions () at jmp_2flow.cpp:31
31	    printf("[thread0] terminated, age=%d, name=%s
", age, name); (gdb) i locals ret = 1 age = 10000 name = 0x2b6700000001 <error: Cannot access memory at address 0x2b6700000001>

이때thread1에서 돌아왔을 때thread0의name이 완전히 변했습니다.
따라서longjmp는stack이 쌓이지 않았을 때 파괴된 창고로 이동할 수 없습니다.예:
main(setjmp) => func1 => func2 (longjmp to main)
func2 만약longjmp가main에 도착하면 문제가 없습니다. 이때 func2의 창고는 사용할 수 없지만 main은 파괴되지 않았습니다.
다음 점프 경로를 가정합니다.
main => func1 => func2 (setjmp)
                           => func3 (longjmp to func2)
func2가 돌아왔습니다. 그리고 func3가longjmp가func2에 도착했을 때 창고의 바늘은func2가setjmp에 있을 때와 같지만 내용은 바뀌었습니다.이럴 때 거의 틀릴 때가 있다.
즉, 스택이 더미에 분배되지 않으면 모든 라인에 자신의 스택이 있을 때 setjmp의 그 함수는 다시 longjmp로 돌아올 수 없습니다. 이때 스택이 파괴되었을 것입니다.
마지막 결론은 st는 각각thread마다stack을 분배해야 한다는 것이다.

좋은 웹페이지 즐겨찾기