首页 > 其他 > 详细

[转载]state thread

时间:2015-11-22 18:51:03      阅读:279      评论:0      收藏:0      [点我收藏+]

原地址:http://www.d0evi1.com/?p=1396

就目前的了解和交流,也发现有好多公司也开始采用“协程”来完成服务器端的开发。传统的服务器端开发模型是:采用session异步方式调用来(调用其它好多服务时需要存状态,来完成一个完整的流程调用),这种方式稍稍复杂一点。采用”协程”的好处,常常被人提起的是:回归“同步调用型”的开发模型:程序调用api1(),api1返回之后调api2(),等api2返回之后调api3.. and so on. 这样的开发模式更加容易理解。

一、setjmp/longjmp

在python/golang/lua中,都内置了协程的实现。但是c语言本身没有协程的实现,不过还好,目前有好多实现coroutine的库,其内部实现原理,是采用setjmp/longjmp来完成的。关于setjmp/longjmp的内容,可以参考APUE的第七章:进程环境。简单的说,就是利用setjmp保存寄存器状态(包括栈),然后利用longjmp恢复到之前的运行栈。这些状态都保存在一个类型为jmp_buf的全局变量中。

简单地看下linux的实现(对比起来:mac下其实更简单明了,注释部分就直接注明):

/usr/include/x86_64-linux-gnu/bits/setjmp.h

/usr/include/setjmp.h   (反着看)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# if __WORDSIZE == 64
typedef long int                     __jmp_buf[8];
# elif defined __x86_64__
__extension__ typedef long long int  __jmp_buf[8];
# else
typedef int                          __jmp_buf[6];
# endif
 
...
 
struct __jmp_buf_tag
{
__jmp_buf        __jmpbuf;           /* Calling environment. */
int              __mask_was_saved;   /* Saved the signal mask? */
__sigset_t       __saved_mask;       /* Saved signal mask. */
};
 
...
 
typedef struct __jmp_buf_tag         jmp_buf[1];

1.why 8 (64)? why 6(32)?

2.setjmp模式有个难点就是,发果对对应的变量理解不够深刻,当恢复栈位置时,相应的变量可能会发生更改,容易出错。因而编程时需要注意。

二、state threads

本文的分析主要基于state threads实现进行分析。为求简单,忽略掉一些平台强相关的逻辑。

2.1 事件系统(_st_eventsys_t)

常见的方式:poll/select/epoll/kqueue等等(默认采用select)。实现方式和其它的一些事件系统(ae/libevent)有点类型,所以此文不再详细地做剖析。

1
2
3
4
5
6
7
8
9
10
11
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;

2.2 协程态(_st_vp_t)

每个vp对应一个idle协程,4个队列,切换回调等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef struct _st_vp {
 _st_thread_t*  idle_thread;    /* Idle thread for this vp */
 st_utime_t     last_clock;     /* The last time we went into vp_check_clock() */
 
 _st_clist_t    run_q;         /* run queue for this vp */
 _st_clist_t    io_q;          /* io queue for this vp */
 _st_clist_t    zombie_q;      /* zombie queue for this vp */
#ifdef DEBUG
 _st_clist_t    thread_q;      /* all threads of this vp */
#endif
 int            pagesize;
 
_st_thread_t *  sleep_q;        /* sleep queue for this vp */
 int            sleepq_size;    /* number of threads on sleep queue */
 
#ifdef ST_SWITCH_CB
 st_switch_cb_t switch_out_cb;  /* called when a thread is switched out */
 st_switch_cb_t switch_in_cb;   /* called when a thread is switched in */
#endif
} _st_vp_t;

2.3 队列(run/io/zombie/sleep queue)

st使用了4个队列, 这个在上面的结构中就已经有,为了访问的方便,定义了4组宏(debug模式下,还有另一个),如下。这4个队列分别对应了4种状态,由此形成自己的状态机体系。

每当创建一个微线程时,都会将其加入到RUNQ中。

1
2
3
4
5
6
7
#define _ST_RUNQ    (_st_this_vp.run_q)
#define _ST_IOQ     (_st_this_vp.io_q)
#define _ST_ZOMBIEQ (_st_this_vp.zombie_q)
#ifdef DEBUG
#define _ST_THREADQ (_st_this_vp.thread_q)
#endif
#define _ST_SLEEPQ  (_st_this_vp.sleep_q)

2.4 微线程(_st_thread_t)

在vp中,我们看到有一个成员:idle_thread. 自然会有疑问:这货是干嘛的?这货长的像个线程,但其实当然不是线程,就以“微线程”来称吧:即用户态下实现的线程。

主要关注点:

  1. 微线程的状态机
  2. 每个微线程维护的栈空间、私有数据
  3. 每个微线程的入口点
  4. 我们熟悉的jmp_buf上下文:context
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
typedef struct _st_thread _st_thread_t;
 
struct _st_thread {
 int     state; /* Thread‘s state */
 int     flags; /* Thread‘s flags */
 
 void *   (*start)(void *arg); /* The start function of the thread */
 void *   arg;                 /* Argument of the start function */
 void *   retval;             /* Return value of the start function */
 
_st_stack_t *   stack;       /* Info about thread‘s stack */
 
_st_clist_t     links;       /* For putting on run/sleep/zombie queue */
 _st_clist_t    wait_links;  /* For putting on mutex/condvar wait queue */
#ifdef DEBUG
 _st_clist_t    tlink;       /* For putting on thread queue */
#endif
 
 st_utime_t     due;      /* Wakeup time when thread is sleeping */
 _st_thread_t * left;     /* For putting in timeout heap */
 _st_thread_t * right;    /* -- see docs/timeout_heap.txt for details */
 int            heap_index;
 
void **         private_data; /* Per thread private data */
 
_st_cond_t *    term;       /* Termination condition variable for join */
 
jmp_buf         context;    /* Thread‘s context */
};

2.5 栈(_st_stack_t)

每个微线程自身单独都会维护自己的一个“栈空间”。栈有自己独立的内存,大小,栈底,栈顶,栈指针(程序级别、非系统内存级别),恢复点。这里的“栈”只是一个概念(实际是堆,通过malloc或mmap来实现),并非我们通常指的栈。

1
2
3
4
5
6
7
8
9
10
11
12
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 */
#ifdef __ia64__
void *          bsp;                /* Register stack backing store pointer */
#endif
} _st_stack_t;

_st_stack的可用空间大小缺省为:128k,即刚开始分配时的大小。当不够用时,会增加一个内存页的大小(getpagesize),当然的大小必须是内存页的整数倍。(详见st_thread_create)。这部分是可用栈空间的大小,实际大小还会加上:2*REDZONE+extra。 在stack的每一端(栈顶、栈底)都有一个REDZONE,其大小也是一个分页;而extra则看是否需要(0或者一个分页大小)

在st_stack中,入栈的方式,有两种:向下增长,向上增加。以向上增长为例(向下增长的话,类似,反之即可):

技术分享

2.6 jmp_buf上下文

即然栈是自己实现的,那边对应的jmp_buf里头的sp、bsp也需要跟着变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
_ST_INIT_CONTEXT(thread, stack->sp, stack->bsp, _st_thread_main);
...
#define MD_SETJMP(env)           _setjmp(env)
#define MD_LONGJMP(env, val)     _longjmp(env, val)
...
#define MD_INIT_CONTEXT(_thread, _sp, _bsp, _main) \
ST_BEGIN_MACRO \
if (MD_SETJMP((_thread)->context)) \
    _main(); \
memcpy((char *)(_bsp) - MD_STACK_PAD_SIZE, \
        (char *)(_thread)->context[0].__jmpbuf[17] - MD_STACK_PAD_SIZE, \
        MD_STACK_PAD_SIZE); \
(_thread)->context[0].__jmpbuf[0] = (long) (_sp); \
(_thread)->context[0].__jmpbuf[17] = (long) (_bsp); \
ST_END_MACRO

3 微线程的创建

3.1 整体流程

微线程的创立,通过_st_thread_create()来完成。

主要过程如下:

  1. 创建栈,分配空间。
  2. 初始化sp/bsp、私有数据(private_data)以及微线程自身所需空间(thread),入口函数(start)及参数。
  3. 调用setjmp,若成功,则执行_st_thread_main();
  4. 将之前的sp恢复.

其中的第三步,_st_thread_main,也就是微线程自身的执行点,过程也很简单:

  1. 获取当前微线程
  2. 将微线程对应的栈空间位置状态变量置为0(state和flags,转型为valatile,防止longjmp切换时更改值。ps: 这里有个关于二级指针数组[1]的tricks)
  3. 执行微线程的入口函数(start)
  4. 退出微线程(_st_thread_exit)

退出微线程时,会发生一些很奇妙的事情:

  1. 清理工作
  2. 对term做检查,是否已退出。若是,添加thread到僵尸队列:zombieq中, 通知term信号,并切换context(_ST_SWITCH_CONTEXT,清理term
  3. 清理stack(_st_stack_free)
  4. 切换至另一个thread执行(_ST_SWITCH_CONTEXT)
上面就是一个整体的流程。而其中有两个地方,需要特别再关注一下。

3.2 上下文切换

因为涉及到微线程的切换,即然是“切换”,那么这里肯定至少涉及到两个微线程。由于微线程本身处在RUNQ中,切换的话,自然会将next。

  1. 如果runq队列有在运行,切换至next; 如果没有,切换至idle_thread
  2. 将切换后的当前微线程置为run状态
  3. longjmp置相应微线程的jmp_buf处,恢复栈执行
 

4.小结

state thread的代码很简洁,如果知道原理的话,分析起来不困难。另外,它还简单地实现了一个http server。有兴趣的朋友可以做为参照自己实现功能更复杂的http server。

[转载]state thread

原文:http://www.cnblogs.com/ym65536/p/4986308.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!