srs source code analysis 2- analysis of state_threads

Keywords: srs

srs is developed based on collaborative process, and state is used at the bottom_ Threads library. In order to better understand srs, you need to be familiar with state first_ threads. We will not introduce the concepts related to co process here, but simply introduce state_ The core logic of threads.

The following state_thread will be called st for short.

Usage example - echo server

A simple echo server is implemented using st. the following code is very simple. The focus is to understand the use of st.

#include <arpa/inet.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <st.h>

#define LISTEN_PORT 9000

#define ERR_EXIT(m) \
  do {              \
    perror(m);      \
    exit(-1);       \
  } while (0)

void *client_thread(void *arg) {
  st_netfd_t client_st_fd = (st_netfd_t)arg;
  int client_fd = st_netfd_fileno(client_st_fd);

  sockaddr_in client_addr;
  socklen_t client_addr_len = sizeof(client_addr);
  int ret = getpeername(client_fd, (sockaddr *)&client_addr, &client_addr_len);
  if (ret == -1) {
    printf("[WARN] Failed to get client ip: %s\n", strerror(ret));
  }

  char ip_buf[INET_ADDRSTRLEN];
  bzero(ip_buf, sizeof(ip_buf));
  inet_ntop(client_addr.sin_family, &client_addr.sin_addr, ip_buf,
            sizeof(ip_buf));

  while (1) {
    char buf[1024] = {0};
    ssize_t ret = st_read(client_st_fd, buf, sizeof(buf), ST_UTIME_NO_TIMEOUT);
    if (ret == -1) {
      printf("client st_read error\n");
      break;
    } else if (ret == 0) {
      printf("client quit, ip = %s\n", ip_buf);
      break;
    }

    printf("recv from %s, data = %s", ip_buf, buf);

    ret = st_write(client_st_fd, buf, ret, ST_UTIME_NO_TIMEOUT);
    if (ret == -1) {
      printf("client st_write error\n");
    }
  }
}

void *listen_thread(void *arg) {
  while (1) {
    st_netfd_t client_st_fd =
        st_accept((st_netfd_t)arg, NULL, NULL, ST_UTIME_NO_TIMEOUT);
    if (client_st_fd == NULL) {
      continue;
    }

    printf("get a new client, fd = %d\n", st_netfd_fileno(client_st_fd));

    st_thread_t client_tid =
        st_thread_create(client_thread, (void *)client_st_fd, 0, 0);
    if (client_tid == NULL) {
      printf("Failed to st create client thread\n");
    }
  }
}

int main() {
  int ret = st_set_eventsys(ST_EVENTSYS_ALT);
  if (ret == -1) {
    printf("st_set_eventsys use linux epoll failed\n");
  }

  ret = st_init();
  if (ret != 0) {
    printf("st_init failed. ret = %d\n", ret);
    return -1;
  }

  int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
  if (listen_fd == -1) {
    ERR_EXIT("socket");
  }

  int reuse_socket = 1;
  ret = setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &reuse_socket,
                   sizeof(int));
  if (ret == -1) {
    ERR_EXIT("setsockopt");
  }

  struct sockaddr_in server_addr;
  server_addr.sin_family = AF_INET;
  server_addr.sin_port = htons(LISTEN_PORT);
  server_addr.sin_addr.s_addr = INADDR_ANY;

  ret =
      bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr));
  if (ret == -1) {
    ERR_EXIT("bind");
  }

  ret = listen(listen_fd, 128);
  if (ret == -1) {
    ERR_EXIT("listen");
  }

  st_netfd_t st_listen_fd = st_netfd_open_socket(listen_fd);
  if (!st_listen_fd) {
    printf("st_netfd_open_socket open socket failed.\n");
    return -1;
  }

  st_thread_t listen_tid =
      st_thread_create(listen_thread, (void *)st_listen_fd, 1, 0);
  if (listen_tid == NULL) {
    printf("Failed to st create listen thread\n");
  }

  while (1) {
    st_sleep(1);   /*It is used to give up the CPU execution right and reschedule the ready coprocess.*/
  }

  return 0;
}
root@learner:~/tmp/st# gcc main.cpp -lst
root@learner-Lenovo:~/tmp/st# ./a.out 
get a new client, fd = 4
recv from 192.168.30.17, data = hello world
client quit, ip = 192.168.30.17
^C
root@learner:~# nc 192.168.30.17 9000
hello world
hello world
^C

Create a listen process to listen to the connection of the client. When the client connects to the service, a client process will be created for this client to process all requests of this client.

Co process switching

There are two ways to switch processes in St: one is to use the setjmp and longjmp interfaces provided by the system, and the other is to use assembly_ st_md_cxt_save and_ st_md_cxt_restore interface. These two functions are the same in usage as setjmp and longjmp.

The switching of these two modes is essentially the switching of stack frames.

setjmp and longjmp

goto statements in C language can only jump within the current function, not between functions. setjmp() and longjmp() can perform nonlocal jump, that is, the target of jump is a location other than the currently executed function.

The setjmp() function establishes the jump target for the subsequent jump executed by the longjmp() call, which is the location where the program initiates the setjmp() call. From a programming point of view, calling the longjmp () function looks exactly like returning from the second call to setjmp (). Through the return value of setjmp (), you can distinguish whether the setjmp () call is the initial return or the second return. The return value of the initial call is 0, and the return value of the subsequent "pseudo return" is any value specified by the val parameter in the longjmp() call. By using different values for the val parameter, it is possible to distinguish different take-off positions that jump to the same target in the program. For more information about setjmp() and longjmp(), please refer to Linux/UNIX system programming manual Page 106 of Volume I.

The following is an example extracted from the Linux/UNIX System Programming Manual:

#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>

jmp_buf env;

void f2(int num)
{
    longjmp(env, num);
}

void f1(int num)
{
    if(num == 1){
        longjmp(env, num);
    }

    f2(num);
}

int main(int argc, char** argv)
{
    if(argc != 2){
        printf("Usage: %s [1|2]\n", argv[0]);
        return -1;
    }

    switch(setjmp(env)){
        case 0:
            printf("Calling f1() after initial setjmp()\n");
            f1(atoi(argv[1]));
            break;
        case 1:
            printf("We jumped back from f1()\n");
            break;
        case 2:
            printf("We jumped back from f2()\n");
            break;
    }

    return 0;
}

I made some modifications to this example. The running results and analysis are as follows:

root@learner:~/tmp# ./a.out 1
Calling f1() after initial setjmp()
We jumped back from f1()

root@learner:~/tmp# ./a.out 2
Calling f1() after initial setjmp()
We jumped back from f2()

_ st_md_cxt_save and_ st_md_cxt_restore

These two functions are implemented through assembly, and the code is as follows:

#define JB_BX  0
#define JB_SI  1
#define JB_DI  2
#define JB_BP  3
#define JB_SP  4
#define JB_PC  5

.file "md.S"
.text

/* _st_md_cxt_save(__jmp_buf env)              Store function stack frame   */
.globl _st_md_cxt_save
    .type _st_md_cxt_save, @function
    .align 16
_st_md_cxt_save:
    movl 4(%esp), %eax                /*Get the address of the parameter env and save it to eax.*/

    movl %ebx, (JB_BX*4)(%eax)        /*Save ebx*/
    movl %esi, (JB_SI*4)(%eax)        /*Save esi*/
    movl %edi, (JB_DI*4)(%eax)        /*Save edi*/
    
    /*Save esp, that is, the top of the stack. The saved top of the stack is not called_ st_ md_ cxt_ Top of stack before save() function*/
    leal 4(%esp), %ecx                /
    movl %ecx, (JB_SP*4)(%eax)        /*Save ecx*/
    movl 0(%esp), %ecx                      
    movl %ecx, (JB_PC*4)(%eax)        /*Save reference counter pc*/
    movl %ebp, (JB_BP*4)(%eax)        /*Called upon saving ebp_ st_ md_ cxt_ ebp of the function of save()*/
    xorl %eax, %eax                   /*Empty eax as_ st_ md_ cxt_ Return value of save()*/
    ret
.size _st_md_cxt_save, .-_st_md_cxt_save

/* _st_md_cxt_restore(__jmp_buf env, int val)     Recover function stack frame  */
.globl _st_md_cxt_restore
    .type _st_md_cxt_restore, @function
    .align 16
_st_md_cxt_restore:
    movl 4(%esp), %ecx            /*Get the address of the first parameter, that is, the address of env.*/
    movl 8(%esp), %eax            /*Get the address of the second parameter, that is, the address of val.*/
    movl (JB_PC*4)(%ecx), %edx    /*Save the value of the original pc register to edx*/

    movl (JB_BX*4)(%ecx), %ebx    /*Restore ebx*/
    movl (JB_SI*4)(%ecx), %esi    /*Restore esi*/
    movl (JB_DI*4)(%ecx), %edi    /*Restore edi*/
    movl (JB_BP*4)(%ecx), %ebp    /*Restore ebp*/
    movl (JB_SP*4)(%ecx), %esp    /*Restore esp*/
    testl %eax, %eax              /*Test whether the value of eax is 0, that is, whether the second parameter is 0.*/
    jnz  1f                       /*If the second parameter is not 0, jump directly to 1: execution.*/
    incl %eax                     /*Set the return value to 1*/
    1: jmp *%edx                  /*Jump to previous pc*/
.size _st_md_cxt_restore, .-_st_md_cxt_restore

_ st_ md_ cxt_ Save (_jmp_buf Env) is used to save stack frames_ st_ md_ cxt_ Restore (_jmp_buf Env, int VAL) is used to restore stack frames.

Switching macro in st

#if defined(MD_USE_BUILTIN_SETJMP) && !defined(USE_LIBC_SETJMP)   
    #define MD_SETJMP(env) _st_md_cxt_save(env)
    #define MD_LONGJMP(env, val) _st_md_cxt_restore(env, val)

    extern int _st_md_cxt_save(jmp_buf env);
    extern void _st_md_cxt_restore(jmp_buf env, int val);
#else
    #define MD_SETJMP(env) setjmp(env)  
    #define MD_LONGJMP(env, val) longjmp(env, val)
#endif

If MD is defined_ USE_ BUILTIN_ Setjmp macro and no use defined_ LIBC_ Setjmp macro, the custom stack frame access function is used. Otherwise, use setjmp and longjmp provided by the system to switch stack frames.

#define _ST_SWITCH_CONTEXT(_thread)       \
    ST_BEGIN_MACRO                        \
    ST_SWITCH_OUT_CB(_thread);            \
    if (!MD_SETJMP((_thread)->context)) { \   /*The transfer out process returns 0 and the transfer in process returns 1.*/ 
        _st_vp_schedule();                \   /*Select the next collaboration to be scheduled*/
    }                                     \
    ST_DEBUG_ITERATE_THREADS();           \
    ST_SWITCH_IN_CB(_thread);             \
    ST_END_MACRO

_ ST_SWITCH_CONTEXT is used to let out the CPU execution right of the collaboration process and reschedule a new collaboration process.

When the coroutine calls_ ST_SWITCH_CONTEXT, MD_SETJMP will return 0, then enter the co process scheduling function_ st_vp_schedule(), the execution authority of the CPU is transferred to other processes. This is equivalent to marking a switching point in this process. When this process will obtain the CPU execution right again, in_ st_ vp_ Call in schedule () ST_RESTORE_CONTEXT macro function, through MD_SETJMP returns again. At this time, the return value is 1. Skip the if statement and return to this procedure call_ ST_ SWITCH_ The position of context and continue to execute.

#define _ST_RESTORE_CONTEXT(_thread)   \
    ST_BEGIN_MACRO                     \
    _ST_SET_CURRENT_THREAD(_thread);   \      /*Mark this collaboration as the currently running collaboration*/
    MD_LONGJMP((_thread)->context, 1); \      /*Perform a process switch to restore the suspended processes before*/
    ST_END_MACRO

_ ST_RESTORE_CONTEXT is used to restore the specified cooperation process through MD_LONGJMP macro, return to MD_ At the breakpoint hit by setjmp, from MD_SETJMP returns again to obtain the execution right of the CPU again.

void _st_vp_schedule(void)
{
    _st_thread_t *thread;
    
    /*Take a process from the ready process queue*/
    if (_ST_RUNQ.next != &_ST_RUNQ) {
        thread = _ST_THREAD_PTR(_ST_RUNQ.next);
        _ST_DEL_RUNQ(thread);    /*Delete from ready process queue*/
    } else {   /*If the ready process queue is empty, all ready processes have been processed.*/
        thread = _st_this_vp.idle_thread;     /*Now switch to idle coroutine*/
    }
    ST_ASSERT(thread->state == _ST_ST_RUNNABLE);    /*The collaboration must be in an operable state*/

    thread->state = _ST_ST_RUNNING;     /*Mark the status of the upcoming collaboration as running*/
    _ST_RESTORE_CONTEXT(thread);        /*Switch to the new process*/
}

When switching a collaboration, a collaboration will be taken from the ready collaboration queue, and then switched to the collaboration. If there are no switchable processes in the ready queue, it means that there are no processes to be processed, and it will be switched to idle processes at this time. After returning to the idle collaboration, it will re-enter epoll_wait, restart listening for events to occur and processing scheduled events.

Scheduler

All the collaborations are executed in a single thread, so a scheduler is needed to schedule all the collaborations, so that the collaborations requiring execution permission can obtain the CPU. Generally, the execution permission is required only when read events, write events and timer events occur. That is, after these events occur, the collaboration needs to be scheduled to the CPU to obtain the execution right of the CPU and handle the corresponding things.

The monitoring of read and write events in st is realized through epoll, and the timer event is realized through the minimum heap and the timeout of epoll.

typedef struct _st_eventsys_ops {
    const char *name;                          /* Name of this event system */
    int  val;                                  /* Type of this event system */
    int  (*init)(void);                        /* Initialization */
    void (*dispatch)(void);                    /* Dispatch function */
    int  (*pollset_add)(struct pollfd *, int); /* Add descriptor set */
    void (*pollset_del)(struct pollfd *, int); /* Delete descriptor set */
    int  (*fd_new)(int);                       /* New descriptor allocated */
    int  (*fd_close)(int);                     /* Descriptor closed */
    int  (*fd_getlimit)(void);                 /* Descriptor hard limit */
} _st_eventsys_t;

This is the interface of the scheduler, which can be implemented by epoll, select and poll.

static _st_eventsys_t _st_epoll_eventsys = {
    "epoll",
    ST_EVENTSYS_ALT,
    _st_epoll_init,
    _st_epoll_dispatch,
    _st_epoll_pollset_add,
    _st_epoll_pollset_del,
    _st_epoll_fd_new,
    _st_epoll_fd_close,
    _st_epoll_fd_getlimit
};

The scheduler is implemented by epoll in st, and these functions are encapsulated into the structure as callback functions.

ST_HIDDEN void _st_epoll_dispatch(void)
{
...
    if (_ST_SLEEPQ == NULL) {     
     /* If the timing queue is empty, it indicates that there is no timer event, then epoll_ The timeout of wait is - 1,
        That is, when no event is triggered, epoll_wait has been blocked.*/
        timeout = -1;
    } else {                      
        /*Get minimum timer from timer queue*/
        min_timeout = (_ST_SLEEPQ->due <= _ST_LAST_CLOCK) ? 0 : (_ST_SLEEPQ->due - _ST_LAST_CLOCK);
        /*Convert epoll_ Timeout of wait unit: us */
        timeout = (int) (min_timeout / 1000);
...
    }

    /*Enter epoll to wait for the trigger of the event, or exit due to timeout.*/
    nfd = epoll_wait(..., ..., ..., timeout);
...
    
    pq->thread->state = _ST_ST_RUNNABLE;  /*Set the status of the collaboration process to the runnable status*/
    _ST_ADD_RUNQ(pq->thread);             /*Add the collaboration to the run queue and wait for a new round of scheduling.*/
...
}

Entering epoll_ Before wait, obtain the time when the latest timer is triggered from the minimum heap and take this time as epoll_ Timeout of wait. If a read / write event occurs within this timeout, epoll_wait returns the processing of read / write events; If no read / write event occurs within the timeout period, epoll_wait will exit because of timeout. At this time, it returns to handle the timed event.

If not from epoll due to timeout_ Wait returns, indicating that some collaboration read-write events are triggered. At this time, it is necessary to save the collaboration that triggered the event to the runnable queue and wait for a new round of scheduling.

Create collaboration

_st_thread_t *st_thread_create(void *(*start)(void *arg), void *arg, int joinable, int stk_size)
{
    _st_thread_t *thread;
    _st_stack_t *stack;
    void **ptds;
    char *sp;
    
    /* Adjust stack size   Adjust stack size*/
    if (stk_size == 0)
        stk_size = ST_DEFAULT_STACK_SIZE;    /*The default stack size is 128KB*/

    /*Page size alignment*/
    stk_size = ((stk_size + _ST_PAGE_SIZE - 1) / _ST_PAGE_SIZE) * _ST_PAGE_SIZE;    
    /*Application stack space*/
    stack = _st_stack_new(stk_size);
    if (!stack)
        return NULL;
    
    sp = stack->stk_top;                     /*Stack top*/
    sp = sp - (ST_KEYS_MAX * sizeof(void *));/*An area is vacated at the top of the stack for private data.*/
    ptds = (void **) sp;
    sp = sp - sizeof(_st_thread_t);          /*One more_ st_thread_t Size*/
    thread = (_st_thread_t *) sp;
    
    if ((unsigned long)sp & 0x3f)
        sp = sp - ((unsigned long)sp & 0x3f);
    
    stack->sp = sp - _ST_STACK_PAD_SIZE;     /*Another 128 bytes of filling area is left at the top of the stack*/

    memset(thread, 0, sizeof(_st_thread_t));
    memset(ptds, 0, ST_KEYS_MAX * sizeof(void *));
    
    thread->private_data = ptds;     /*Private data pointing to co process*/
    thread->stack = stack;           /*Point to the process stack*/
    thread->start = start;           /*Co process entry function*/
    thread->arg = arg;               /*Entry function parameters*/
    
    /*Save the switching context and mark the restore point. When this process obtains the execution permission next time, it will execute from this restore point.*/
    _ST_INIT_CONTEXT(thread, stack->sp, _st_thread_main);
    
    /*If you need to actively reclaim a collaboration process, you need to create a condition variable to block the collaboration process waiting for recycling.*/
    if (joinable) {     
        thread->term = st_cond_new();
        if (thread->term == NULL) {
            _st_stack_free(thread->stack);
            return NULL;
        }
    }
    
    thread->state = _ST_ST_RUNNABLE;   /*Mark the collaboration as runnable*/
    _st_active_count++;                /*Increase the number of active processes*/
    _ST_ADD_RUNQ(thread);              /*Insert a collaboration into the run queue*/

    return thread;
}

Create a new collaboration. During the creation process, the collaboration will be placed in the runnable queue and waiting for scheduling. When scheduling this new collaboration, you can obtain the execution right of the CPU.

Except for the main process, the stack of other processes is the space requested on the heap. The default size is 128KB.

#define _ST_INIT_CONTEXT MD_INIT_CONTEXT

#define MD_INIT_CONTEXT(_thread, _sp, _main) \
	ST_BEGIN_MACRO                           \
	if (MD_SETJMP((_thread)->context))       \ /*Set or return from a restore point.*/
	      _main();                           \ 
	MD_GET_SP(_thread) = (long) (_sp);       \ /*Set the value of sp register in ctx and set a new stack frame*/
	ST_END_MACRO

When creating a new coroutine, the restore point will be set through the macro function above. When it is executed to MD_ When setjmp, it will return 0. At this time_ The main() function will not be executed. When the cooperative process obtains the execution right again, it will be transferred from the MD again_ Setjmp returns. If the return value is 1, enter_ The main () function, that is_ st_thread_main() function.

void _st_thread_main(void)
{
    _st_thread_t *thread = _ST_CURRENT_THREAD();      /*Gets the handle of the current collaboration*/
    
    MD_CAP_STACK(&thread);
    
    thread->retval = (*thread->start)(thread->arg);   /*Execute co process entry function*/
    
    st_thread_exit(thread->retval);                   /*Co process exit*/
}

After the new collaboration is created, it will not be executed immediately. You need to mark the restore point first and then put it into the executable queue. When the scheduler schedules this new thread, it will really obtain the execution right of the CPU. In MD_ After setjmp returns, enter this function, and then enter the entry function of the coroutine. After the co process entry function is processed, it will enter the co process exit function, which will be analyzed later.

Initialization of st

int st_init(void)
{
    _st_thread_t *thread;
    
    if (_st_active_count) {                /*If it has been initialized, it returns directly.*/
        return 0;
    }
    
    st_set_eventsys(ST_EVENTSYS_DEFAULT);  /*Set epoll encapsulated interface */
    
    if (_st_io_init() < 0)
        return -1;
    
    memset(&_st_this_vp, 0, sizeof(_st_vp_t));
    
    /*Initialization of three queues*/
    ST_INIT_CLIST(&_ST_RUNQ);        /*Runnable queue*/
    ST_INIT_CLIST(&_ST_IOQ);         /*io queue*/
    ST_INIT_CLIST(&_ST_ZOMBIEQ);     /*Zombie queue*/
    
    if ((*_st_eventsys->init)() < 0)  /*epoll Initialization of*/
        return -1;
    
    _st_this_vp.pagesize = getpagesize();     /*Page size*/
    _st_this_vp.last_clock = st_utime();      /*Clock time*/
    
    /* Create an idle collaboration */
    _st_this_vp.idle_thread = st_thread_create(_st_idle_thread_start, NULL, 0, 0);
    if (!_st_this_vp.idle_thread)
        return -1;

    _st_this_vp.idle_thread->flags = _ST_FL_IDLE_THREAD;    /*Identified as idle collaboration*/
    _st_active_count--;      

    _ST_DEL_RUNQ(_st_this_vp.idle_thread);    /*Delete an idle procedure from the runnable queue*/
    
    /*Encapsulate one for the main process_ st_thread_t */
    thread = (_st_thread_t *) calloc(1, sizeof(_st_thread_t) + (ST_KEYS_MAX * sizeof(void *)));    
    if (!thread)
        return -1;

    thread->private_data = (void **) (thread + 1);    /*Private data pointing to co process*/
    thread->state = _ST_ST_RUNNING;                   /*Set the collaboration to the runnable state*/
    thread->flags = _ST_FL_PRIMORDIAL;                /*Identify the main process*/

    _ST_SET_CURRENT_THREAD(thread);       /*Set the collaboration of the current work*/
    _st_active_count++;                   /*Increase the number of active processes*/

    return 0;
}

When using st, you first need to call st_ The init() function initializes st. This function has three functions: 1. Do some initialization work; 2. Create an idle coroutine; 3. Encapsulate the main thread as the main coroutine

The main thread is also an executable flow. It needs to be encapsulated into a main co process so that it can be scheduled in the scheduler.

The idle coroutine is very core. When there is no runnable coroutine in the ready queue, the execution permission of the CPU will be scheduled to the idle coroutine. Restart listening for read, write, and timer events in the idle coroutine.

void *_st_idle_thread_start(void *arg)
{
    _st_thread_t *me = _ST_CURRENT_THREAD();
    
    while (_st_active_count > 0) {
        _ST_VP_IDLE();                /*Enter epoll_wait, listen for read / write events*/
        
        _st_vp_check_clock();         /*Processing timer events*/
        
        me->state = _ST_ST_RUNNABLE;  /*Mark idle threads as runnable*/
        _ST_SWITCH_CONTEXT(me);       /*Give up the CPU execution right and restart scheduling.*/
    }
    
    exit(0);
    
    return NULL;
}

When the ready queue is empty, the scheduling will enter the idle thread and epoll in the idle thread_ Wait listens for read-write events. When a read-write event is triggered, the collaboration will be saved to the ready queue; From epoll_ After the wait returns, check whether there is a timer trigger. If there is a timer trigger, save the collaboration to the ready queue. After the read / write events and timer events are processed, the idle process gives up the CPU execution right and starts scheduling all ready processes in turn. After all ready processes are processed, it will enter the idle process again, and then it will go back and forth.

#define _ST_VP_IDLE()                   (*_st_eventsys->dispatch)()

_ st_ Eventsys - > dispatch is a callback function whose pointer actually points to_ st_epoll_dispatch.

void _st_vp_check_clock(void)
{
    _st_thread_t *thread;
    st_utime_t now;

    now = st_utime();        /*Get current time*/
    _ST_LAST_CLOCK = now;
    
    if (_st_curr_time && now - _st_last_tset > 999000) {
        _st_curr_time = time(NULL);
        _st_last_tset = now;
    }
    
    while (_ST_SLEEPQ != NULL) {    /*Sleep queue is not empty*/
        thread = _ST_SLEEPQ;        /*Gets the smallest timer on the smallest heap*/
        ST_ASSERT(thread->flags & _ST_FL_ON_SLEEPQ);

        if (thread->due > now)    
            break;                  /*The timer of the co process has not arrived yet. Return immediately.*/

        /*The timer of the coroutine is triggered*/
        _ST_DEL_SLEEPQ(thread);  /*Remove from sleep queue*/
        
        /*The coprocessor slept because of the conditional variable. Now the conditional variable timed out.*/
        if (thread->state == _ST_ST_COND_WAIT)    
            thread->flags |= _ST_FL_TIMEDOUT;     
        
        ST_ASSERT(!(thread->flags & _ST_FL_IDLE_THREAD));
        
        thread->state = _ST_ST_RUNNABLE;    /*Mark the collaboration as runnable*/
        _ST_INSERT_RUNQ(thread);            /*Send the coordination process to the ready queue and wait for scheduling.*/
    }
}

From epoll_ After the wait returns, check the collaboration in the sleep queue. When the timer reaches, send the collaboration to the ready queue and wait for a new round of scheduling.

All timers are placed in the minimum heap, and the minimum value of all timers is obtained from the minimum heap. If the current time exceeds the timer in the minimum heap, the timer is triggered. Save all the triggered timers in the minimum heap to the ready queue through the while loop.

exit, join, and yield of the collaboration

_st_thread_t *st_thread_create(void *(*start)(void *arg), void *arg, int joinable, int stk_size)
{
...
	if (joinable) {     /*If the collaboration needs active recycling, create a condition variable for the collaboration.*/
		thread->term = st_cond_new();        /*Create condition variable*/
		if (thread->term == NULL) {
			_st_stack_free(thread->stack);
			return NULL;
		}
    }
...
}

When creating a collaboration process, you need to indicate whether the collaboration process will be actively recycled. If you need to actively reclaim a process, you need to create a condition variable for the process to reclaim the process blocked by other processes.

void _st_thread_main(void)
{
    _st_thread_t *thread = _ST_CURRENT_THREAD();      /*Gets the handle of the current collaboration*/
    
    MD_CAP_STACK(&thread);
    
    thread->retval = (*thread->start)(thread->arg);   /*Execute co process entry function*/
    
    st_thread_exit(thread->retval);                   /*Exit process*/
}

After the main function of the coroutine is executed, it will enter st_thread_exit function, used to exit the coroutine.

void st_thread_exit(void *retval)
{
    _st_thread_t *thread = _ST_CURRENT_THREAD();    /*Gets the current collaboration handle*/
    
    thread->retval = retval;        /*Save return value*/
    _st_thread_cleanup(thread);     /*Release the private data of the process*/
    _st_active_count--;             /*Number of active processes minus one*/
    if (thread->term) {    /*If you need to actively recycle this process*/
        thread->state = _ST_ST_ZOMBIE;     /*Set the collaboration to zombie state*/
        _ST_ADD_ZOMBIEQ(thread);           /*Add to zombie queue*/
        
        st_cond_signal(thread->term);      /*Notifies that a process waiting for collection is blocked*/
        
        _ST_SWITCH_CONTEXT(thread);        /*Cede the right of execution*/
        
        st_cond_destroy(thread->term);     /*Destroy condition variable*/
        thread->term = NULL;
    }
    
    /*If it is a main process, there is no need to release its corresponding stack, otherwise the stack space applied on the heap will be released.*/
    if (!(thread->flags & _ST_FL_PRIMORDIAL))
        _st_stack_free(thread->stack);
    
    _ST_SWITCH_CONTEXT(thread);            /*After destruction, give up the CPU execution right*/
}

If the coroutine is the main coroutine, there is no need to release the heap space, otherwise the space for stack applied on the heap needs to be released. Thread - > term is not NULL, which means that the collaboration needs to be actively recycled. At this time, the collaboration needs to be set to zombie state and added to the zombie state queue. At the same time, it notifies the coprocess blocking waiting for recycling.

int st_thread_join(_st_thread_t *thread, void **retvalp)
{
    _st_cond_t *term = thread->term;         /*Gets the conditional variable of the coroutine*/
    if (term == NULL) {                  
        errno = EINVAL;
        return -1;
    }
    
    if (_ST_CURRENT_THREAD() == thread) {    /*Cannot be the current collaboration*/
        errno = EDEADLK;
        return -1;
    }
    
	/*Multiple threads cannot recycle the same coroutine at the same time*/
    if (term->wait_q.next != &term->wait_q) {
        errno = EINVAL;
        return -1;
    }
    
    /*If the state of the coroutine is not zombie, the thread used for recycling will enter the condition variable and wait.*/
    while (thread->state != _ST_ST_ZOMBIE) {
        if (st_cond_timedwait(term, ST_UTIME_NO_TIMEOUT) != 0)
            return -1;
    }
    
    if (retvalp)
        *retvalp = thread->retval;       /*Get the return value of the collaboration to be recycled*/
    
    thread->state = _ST_ST_RUNNABLE;     /*Set the collaboration to be recycled to the operable state*/
    _ST_DEL_ZOMBIEQ(thread);             /*Delete from zombie queue*/
    _ST_ADD_RUNQ(thread);                /*Join ready to run queue*/
    
    return 0;
}

When a collaboration process is recycling another collaboration process, the collaboration process to be recycled has not yet exited, and the actively recycled collaboration process will enter the condition variable and wait. When the co process to be recycled exits, the co process on the condition variable will be activated.

After the actively recycled collaboration returns from the condition variable, the collaboration to be recycled is in zombie state. After obtaining the return value, you need to set the collaboration to be recycled to runnable state again and join the ready to run queue. The process to be recycled will enter st again_ thread_ Exit() function, from_ ST_SWITCH_CONTEXT returns, actively destroys condition variables and stack space, and finally passes_ ST_SWITCH_CONTEXT gives up the execution right, and then the cooperation process is considered to exit.

void st_thread_yield()
{
    _st_thread_t *me = _ST_CURRENT_THREAD();    /*Gets the current collaboration handle*/

    /*Check whether a timer event is triggered*/
    _st_vp_check_clock();

    /*If the ready queue is empty, it will be returned directly.*/
    if (_ST_RUNQ.next == &_ST_RUNQ) {
        return;
    }

    me->state = _ST_ST_RUNNABLE;  /*Mark this process as operable*/
    _ST_ADD_RUNQ(me);             /*Add this process to the ready queue*/

    /*Switch the execution right to other coprocesses in the ready queue*/
    _ST_SWITCH_CONTEXT(me);
}

In the process of running, the coordination process can actively give up the right of execution. When giving up the execution right, you need to actively add yourself to the ready queue and wait to be scheduled again.

socket processing

int st_poll(struct pollfd *pds, int npds, st_utime_t timeout)
{
    struct pollfd *pd;
    struct pollfd *epd = pds + npds;     /*Point to the end of the array*/
    _st_pollq_t pq;
    _st_thread_t *me = _ST_CURRENT_THREAD();
    int n;
    
    if (me->flags & _ST_FL_INTERRUPT) {
        me->flags &= ~_ST_FL_INTERRUPT;
        errno = EINTR;
        return -1;
    }
    
    if ((*_st_eventsys->pollset_add)(pds, npds) < 0)
        return -1;
    
    pq.pds = pds;
    pq.npds = npds;
    pq.thread = me;
    pq.on_ioq = 1;
    _ST_ADD_IOQ(pq);
    if (timeout != ST_UTIME_NO_TIMEOUT)
        _ST_ADD_SLEEPQ(me, timeout);
    me->state = _ST_ST_IO_WAIT;
    
    /*Take the initiative to cut out the cooperation process and hand over the executive power.*/
    _ST_SWITCH_CONTEXT(me);
    
    n = 0;
    if (pq.on_ioq) {
        _ST_DEL_IOQ(pq);
        (*_st_eventsys->pollset_del)(pds, npds);
    } else {
        for (pd = pds; pd < epd; pd++) {
            if (pd->revents)
                n++;
        }
    }
    
    if (me->flags & _ST_FL_INTERRUPT) {
        me->flags &= ~_ST_FL_INTERRUPT;
        errno = EINTR;
        return -1;
    }
    
    return n;
}

Register the event that needs to be monitored, and then give up the CPU execution right. When the event is triggered, it will be started again from the_ ST_SWITCH_CONTEXT returns to continue processing.

int st_netfd_poll(_st_netfd_t *fd, int how, st_utime_t timeout)
{
    struct pollfd pd;
    int n;
    
    pd.fd = fd->osfd;
    pd.events = (short) how;
    pd.revents = 0;
    
    if ((n = st_poll(&pd, 1, timeout)) < 0)     /*Single fd*/
        return -1;
    if (n == 0) {
        errno = ETIME;
        return -1;
    }
    if (pd.revents & POLLNVAL) {
        errno = EBADF;
        return -1;
    }
    
    return 0;
}

Encapsulation of a file descriptor

_st_netfd_t *st_accept(_st_netfd_t *fd, struct sockaddr *addr, int *addrlen, st_utime_t timeout)
{
    int osfd, err;
    _st_netfd_t *newfd;
    
    /*Execute the accept function. If there is no client connection, accept returns immediately.*/
    while ((osfd = accept(fd->osfd, addr, (socklen_t *)addrlen)) < 0) {
        if (errno == EINTR)
            continue;
        if (!_IO_NOT_READY_ERROR)
            return NULL;

        /*Enter the poll function, register the read event, give up the execution right of the CPU, and wait for the read event to trigger.*/
        if (st_netfd_poll(fd, POLLIN, timeout) < 0)
            return NULL;
    }
    
    /*accept The returned client socket fd is encapsulated.*/
    newfd = _st_netfd_new(osfd, 1, 1);
    if (!newfd) {
        err = errno;
        close(osfd);
        errno = err;
    }
    
    return newfd;
}

fd is set to non blocking. After calling the accept() function, if no client requests a connection, it will immediately return from accept. If errno is EAGAIN or EWOULDBLOCK, it means that there is no client connection, and then execute st_netfd_poll() function, in which the read event will be registered for fd and the execution right of the CPU will be surrendered. When the read event of fd is triggered, this process will be scheduled again to obtain the CPU execution right, and then execute down.

ssize_t st_read(_st_netfd_t *fd, void *buf, size_t nbyte, st_utime_t timeout)
{
    ssize_t n;
    
    while ((n = read(fd->osfd, buf, nbyte)) < 0) {   /*Non blocking read*/
        if (errno == EINTR)            /*Interrupted by the signal*/
            continue;
        if (!_IO_NOT_READY_ERROR)      /*Not EAGAIN or EWOULDBLOCK error*/
            return -1;

        /*The execution here indicates that an EAGAIN or EWOULDBLOCK error has occurred. At this time, there is no data to read. Give up the execution right.*/
        if (st_netfd_poll(fd, POLLIN, timeout) < 0)
            return -1;
    }
    
    return n;
}

The principle of read is the same as that of accept and will not be repeated.

Posted by micklerlop on Sat, 02 Oct 2021 13:31:54 -0700