原地址:http://www.d0evi1.com/?p=1396
就目前的了解和交流,也发现有好多公司也开始采用“协程”来完成服务器端的开发。传统的服务器端开发模型是:采用session异步方式调用来(调用其它好多服务时需要存状态,来完成一个完整的流程调用),这种方式稍稍复杂一点。采用”协程”的好处,常常被人提起的是:回归“同步调用型”的开发模型:程序调用api1(),api1返回之后调api2(),等api2返回之后调api3.. and so on. 这样的开发模式更加容易理解。
在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实现进行分析。为求简单,忽略掉一些平台强相关的逻辑。
常见的方式: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; |
每个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; |
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) |
在vp中,我们看到有一个成员:idle_thread. 自然会有疑问:这货是干嘛的?这货长的像个线程,但其实当然不是线程,就以“微线程”来称吧:即用户态下实现的线程。
主要关注点:
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 */ }; |
每个微线程自身单独都会维护自己的一个“栈空间”。栈有自己独立的内存,大小,栈底,栈顶,栈指针(程序级别、非系统内存级别),恢复点。这里的“栈”只是一个概念(实际是堆,通过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中,入栈的方式,有两种:向下增长,向上增加。以向上增长为例(向下增长的话,类似,反之即可):
即然栈是自己实现的,那边对应的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 |
微线程的创立,通过_st_thread_create()来完成。
主要过程如下:
其中的第三步,_st_thread_main,也就是微线程自身的执行点,过程也很简单:
退出微线程时,会发生一些很奇妙的事情:
因为涉及到微线程的切换,即然是“切换”,那么这里肯定至少涉及到两个微线程。由于微线程本身处在RUNQ中,切换的话,自然会将next。
原文:http://www.cnblogs.com/ym65536/p/4986308.html