st(state-threads)coroutine와stack분석

9910 단어
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
나는 st를 간소화하여 다른 시스템을 없애고 linux 시스템만 고려하며 i386/x86_64/arm/mips 네 가지 cpu 시리즈, 참고:https://github.com/winlinvip/simple-rtmp-server/tree/master/trunk/research/st본고는 coroutine의 창설과stack의 관리를 소개한다.

STACK 할당


Stack 데이터 구조는 다음과 같이 정의됩니다.
typedef struct _st_stack {
    _st_clist_t links;
    char *vaddr;                /* Base of stack's allocated memory */
    int  vaddr_size;            /* Size of stack's allocated memory */
    int  stk_size;              /* Size of usable portion of the stack */
    char *stk_bottom;           /* Lowest address of stack's usable portion */
    char *stk_top;              /* Highest address of stack's usable portion */
    void *sp;                   /* Stack pointer from C's point of view */
} _st_stack_t;

실제로 vaddr는 창고의 메모리 시작 주소입니다. 다른 몇 개의 주소는 아래에서 분석합니다.
창고의 분배는_st_stack_new 함수, st_thread_create 함수 호출, stack의 사이즈를 계산한 다음 창고를 분배합니다.
    | REDZONE |          stack         |  extra  | REDZONE |
    +---------+------------------------+---------+---------+
    |    4k   |                        |   4k/0  |    4k   |
    +---------+------------------------+---------+---------+
    vaddr     bottom                   top

위 그림은 창고 분배 후의 결과입니다. 양쪽은 REDZONE이 mprotect를 사용하여 접근되지 않도록 보호합니다. (DEBUG가 시작된 후) extra는 추가 메모리 블록입니다. st_randomize_stacks가 열리면bottom과 top를 조정합니다. 무작위로 오른쪽으로 조금 이동합니다.
한 마디로 하면 마지막으로 사용한 대외적으로 제공된 인터페이스는bottom과 top,st_thread_create 함수는 sp를 초기화합니다.Stack이 대외적으로 제공하는 서비스는 [bottom, top] 이 메모리 영역입니다.

THREAD 초기화 스택


Stack을 열면 st는 stack을 초기화하고 분배합니다. 이 stack은 직접thread의 창고가 아니라 다음과 같은 분배를 합니다.
        +--------------------------------------------------------------+
        |                         stack                                |
        +--------------------------------------------------------------+
        bottom                                                         top

할당은 다음과 같습니다.
        +-----------------+-----------------+-------------+------------+
        | stack of thread |pad+align(128B+) |thread(336B) | keys(128B) |
        +-----------------+-----------------+-------------+------------+
        bottom            sp                trd           ptds         top
               (context[0].__jmpbuf.sp)             (private_data)

즉,
ptds: 이거는thread의private_데이터, 12개 포인터(ST_KEYS_MAX 지정), 참조 st_key_create().
trd:thread 구조 자체도 이 stack에서 분배됩니다.
pad+align: trd 다음에 정렬 및 pad(_ST_STACK_PAD_SIZE 지정)입니다.
sp:이것이 바로thread의 진정한 스택입니다.
coroutine는 반드시 스스로 stack을 분배해야 합니다. setjmp는 sp의 값만 저장하고 모든 copy 창고가 없기 때문에 시스템의 stack을 사용하면 각각thread 사이longjmp가 혼동될 수 있습니다.참조:http://blog.csdn.net/win_lin/article/details/40948277

Thread 시작 및 전환


st의thread는 어떻게 지정된 입구로 들어갑니까?
사실 첫 번째 setjmp에서는thread를 초기화했습니다. 이때 반환 값은 0입니다. 초기화가 끝난 후에 호출 함수로 돌아가서 계속 실행했습니다.
호출 함수는 다른 곳에서longjmp를 이thread로 호출합니다. 이 때 setjmp에서 실행됩니다. 반환값은 0이 아닙니다. 이 때 thread에 들어가는 주 함수:_st_thread_main.
내가 변경한 코드를 참고하십시오.
_st_thread_t *st_thread_create(void *(*start)(void *arg), void *arg, int joinable, int stk_size)
{
// by winlin, expend macro MD_INIT_CONTEXT
#if defined(__mips__)
    MD_SETJMP((trd)->context);
    trd->context[0].__jmpbuf[0].__pc = (__ptr_t) _st_thread_main;
    trd->context[0].__jmpbuf[0].__sp = stack->sp;
#else
    int ret_setjmp = 0;
    if ((ret_setjmp = MD_SETJMP((trd)->context)) != 0) {
        _st_thread_main();
    }
    MD_GET_SP(trd) = (long) (stack->sp);
#endif
}

ddb 디버깅, 첫 번째 setjmp 시 반환값은 0이고, 호출 창고는 라인을 만드는 창고이며, 62줄의 코드는 st_thread_t trd = st_thread_create(thread_func, NULL, 1, 0);:
(gdb) f
#0  st_thread_create (start=0x4073fb <thread_func>, arg=0x0, joinable=1, stk_size=65536) at sched.c:600
600	    if ((ret_setjmp = MD_SETJMP((trd)->context)) != 0) {
(gdb) bt
#0  st_thread_create (start=0x4073fb <thread_func>, arg=0x0, joinable=1, stk_size=65536) at sched.c:600
#1  0x00000000004074b5 in thread_test () at srs.c:62
#2  0x00000000004081c3 in main (argc=1, argv=0x7fffffffe4b8) at srs.c:344
(gdb) p ret_setjmp 
$36 = 0

다른 라인에서 전환되었을 때, 즉longjmp가 왔을 때, 반환값은 0이 아닙니다. 호출 창고는longjmp의 창고이고, 68줄의 코드는st_thread_join(trd, NULL);:
(gdb) f
#0  st_thread_create (start=0x4073fb <thread_func>, arg=0x6390b0, joinable=0, stk_size=6599392) at sched.c:601
601	        _st_thread_main();
(gdb) bt
#0  st_thread_create (start=0x4073fb <thread_func>, arg=0x6390b0, joinable=0, stk_size=6599392) at sched.c:601
#1  0x00000000004074f4 in thread_test () at srs.c:68
#2  0x00000000004081c3 in main (argc=1, argv=0x7fffffffe4b8) at srs.c:344
(gdb) p ret_setjmp 
$37 = 1

주의,thread_로 표시되지만test 이 함수 이리 와, 실제 함수 행수가 달라졌어, gdb가 표시하는 stk_size도 파괴되었습니다. 왜냐하면 이때의 창고는 st가 직접 개척한 창고이기 때문입니다.
진입 _st_thread_main에서 사용자가 지정한 스레드 함수를 호출합니다. (이 함수에서 st 함수 setjmp를 호출합니다. 다음에longjmp는 이 위치에 도착합니다.)스레드 함수에서 반환하면 st_thread_exit는 라인을 정리하고 다른 함수로 전환합니다. 마지막 함수가 끝날 때까지 되돌아옵니다.
void _st_thread_main(void)
{
    _st_thread_t *trd = _ST_CURRENT_THREAD();
    
    /* Run thread main */
    trd->retval = (*trd->start)(trd->arg);
    
    /* All done, time to go away */
    st_thread_exit(trd->retval);
}

이것이 바로 st의thread가 시작되고 스케줄링되는 과정입니다.
첫 번째 루틴과 setjmp를 만들면 sp, 즉stack을 설정합니다.즉, 이 함수의 모든 stack 정보는longjmp 이후에 알 수 없습니다. 이것은 모든 st의thread가 끝난 후에 반드시 longjmp를 다른 라인으로 보내거나 종료해야 합니다. 직접return을 할 수 없는 이유입니다. (return이 불가능하기 때문에 최고급 stack은 _st_thread_main입니다.)

Thread 종료


st의thread에서 종료하면 다른thread로 전환됩니다.
st가 만든thread, 끝나면 st_ 호출thread_exit, 참조_st_thread_main의 정의, 이것은thread가 실행하는 주요 절차입니다.
st 초기화 중st_init 시, 현재 라인을 _로 간주합니다ST_FL_PRIMORDIAL, 즉 초기화 라인입니다. 이 라인은 exit를 호출하면 다른thread가 완성될 때까지 기다리면 직접exit입니다.실제로는 라인이 없을 때 idle 라인으로 전환됩니다.
void _st_vp_schedule(void)
{
    _st_thread_t *trd;
    
    if (_ST_RUNQ.next != &_ST_RUNQ) {
        /* Pull thread off of the run queue */
        trd = _ST_THREAD_PTR(_ST_RUNQ.next);
        _ST_DEL_RUNQ(trd);
    } else {
        /* If there are no threads to run, switch to the idle thread */
        trd = _st_this_vp.idle_thread;
    }

idle 스레드는 st_init 시 생성, 즉 st_init는 idle 스레드(st_thread_create 사용)를 생성하고 _ST_FL_PRIMORDIAL 스레드(직접 calloc).idle 스레드의 코드:
void *_st_idle_thread_start(void *arg)
{
    _st_thread_t *me = _ST_CURRENT_THREAD();
    
    while (_st_active_count > 0) {
        /* Idle vp till I/O is ready or the smallest timeout expired */
        _ST_VP_IDLE();
        
        /* Check sleep queue for expired threads */
        _st_vp_check_clock();
        
        me->state = _ST_ST_RUNNABLE;
        _ST_SWITCH_CONTEXT(me);
    }
    
    /* No more threads */
    exit(0);
    
    /* NOTREACHED */
    return NULL;
}

모든 라인이 완성되면 exit입니다.

Thread 초기 스레드


st의 초기 라인, 또는 물리적 라인,primordial 라인, 호출 st_init의 그 라인.일반적으로 st를 호출하는 프로그램은 모두 단일 루트이기 때문에 이 초기 루트는 그 시스템의 유일한 루트이다.
모든 st의 스레드는 st_를 호출합니다.create_thread에서 만든,st가 직접 개척한stack을 사용합니다.초기 라인을 제외하고stack을 다시 설정하지 않았습니다. 이것이 바로 초기 라인 (물리적 라인) 입니다.
참조 st_init 코드:
    /*
    * Initialize primordial thread
    */
    trd = (_st_thread_t *) calloc(1, sizeof(_st_thread_t) +
    (ST_KEYS_MAX * sizeof(void *)));
    if (!trd) {
        return -1;
    }
    trd->private_data = (void **) (trd + 1);
    trd->state = _ST_ST_RUNNING;
    trd->flags = _ST_FL_PRIMORDIAL;
    _ST_SET_CURRENT_THREAD(trd);
    _st_active_count++;

trd 객체를 할당할 때 할당됨_st_thread_t와keys 두 대상은 앞에서 스택에 대한 사용을 참고할 수 있습니다.keys는 private_데이터, 그래서 뒤에 private_ 초기화데이터는 다음thread를 가리킨다.
생성된 후 이 스레드를 _ 로 설정ST_FL_PRIMORDIAL, 이것은 stack이 st가 스스로 분배한 것인지를 가리키는 것이다.
void st_thread_exit(void *retval)
{
    if (!(trd->flags & _ST_FL_PRIMORDIAL)) {
        _st_stack_free(trd->stack);
    }
}

초기 스레드 (물리적 스레드) 라면 stack 은 방출되지 않습니다. 이 stack 은 NULL 입니다.
스케줄링을 할 때, Stack이 직접 만들었든 안 만들었든 스케줄링에 영향을 주지 않습니다.stack이 st 자체로 만든 경우, setjmp 이후의context에서 sp의 주소를 수정할 뿐, 이때longjmp는 새로운stack을 사용할 뿐입니다. longjmp에 대한 jmp_buf는 도대체 sp가 스스로 만든 것이냐 아니면 시스템적인 것이냐는 사실 차이가 없다.
그래서 초기 스레드(물리적 스레드)도 st의thread로 스케줄링되어 아무런 차이가 없습니다.

Thread 라이프 사이클


st 전체 라인의 실행 절차를 다시 정리합니다.
첫 번째 단계, st_init idle 스레드 생성 및priordial 스레드 생성 (초기 스레드, 물리적 스레드, _ST_FL_PRIMORDIAL), 이때_st_active_count는 1, 즉 초기 라인 (st_init 호출, 물리 라인) 이 실행되고 있으며, idle 라인은 하나의 active 라인이 아닙니다. 주로 전환과 종료를 합니다.
두 번째 단계, 선택할 수 있는 단계, 사용자가 라인을 만듭니다.st_ 호출thread_create 시 _st_active_count가 점차 증가하고 스레드 대기열에 추가됩니다.예를 들어 하나의 라인을 만들었다.이때 st 스케줄링은 두 개의 라인이 있는데 하나는 초기 라인이고 하나는 방금 만든 라인입니다.
세 번째 단계, 초기 라인 전환, 제어권을 st에게 건네줍니다.즉 초기 라인, st_init와 다른 루틴을 만든 후 이 때 루틴 전환이 없습니다.초기 스레드(물리적 스레드)는 st에 제어권을 전환해야 합니다. st_를 호출할 수 있습니다sleep 순환과 휴면, 또는 st_ 호출thread_exit(NULL)는 다른 스레드가 끝날 때까지 기다립니다.이 단계의 물리적 라인을 전환하지 않으면 st는 제어권을 얻을 수 없고 프로그램이 직접 되돌아옵니다.
이렇게 설계한 것은 사실 매우 완벽하다. 만약에 물리적 라인이 exit가 아니라면 st의 idle 라인도 물러나지 않는다.만약 초기 라인이 직접 종료된다면idle 라인은 제어권을 얻지 못할 것입니다.초기 스레드가 st_ 호출되면thread_exit(NULL), 물리적 라인도 종료된다고 생각하면idle은 모든 라인이 끝나면 exit를 합니다. 이것은 제어권을 st에게 넘기는 것과 같습니다.
또는 초기 루틴(물리적 루틴)에서 각종 업무 논리를 할 수 있다. 예를 들어 srs는 초기 루틴으로 각종 데이터를 업데이트하여api에 사용할 수 있다.또는 직접 스레드를 생성한 후 st_thread_exit, 모든 라인이 종료될 때까지 기다립니다.

좋은 웹페이지 즐겨찾기