Greenlet
協(xié)程可以算是自定義控制切換的微線程。
棧切換的本質
1.棧
- 棧是從高地址向低地址
- 棧幀(stack frame),機器用棧來傳遞過程參數,存儲返回信息,保存寄存器用于以后恢復,以及本地存儲。為單個過程(函數調用)分配的那部分棧稱為棧幀。棧幀其實是兩個指針寄存器,寄存器%ebp為幀指針,而寄存器%esp為棧指針
2.切換
- 切換其實是切換的執(zhí)行位置(top_frame)。
- 但是當我切換執(zhí)行位置,同時要切換到目的棧,同時要保證棧內數據沒有丟失,且沒有被無意修改。這就需要棧數據的保存與恢復(slp_switch)。
如何進行切換?
1. C棧切換
其實協(xié)程的一個很特殊的例子,就是函數調用。下面這個例子在main中調用func
#include<stdio.h>
int func(int arg)
{
int d=4;
int e=5;
int f;
f=d+e+arg;
return f;
}
int main()
{
int a=1;
int b=2;
int c=3;
func(c);
c=a+b;
}
用gcc生成匯編code,建議在redhat或centos下
.file "stackpointer.c"
.text
.globl func
.type func, @function
func:
pushl %ebp
movl %esp, %ebp
subl $16, %esp
movl $4, -12(%ebp)
movl $5, -8(%ebp)
movl -8(%ebp), %eax
movl -12(%ebp), %edx
leal (%edx,%eax), %eax
addl 8(%ebp), %eax
movl %eax, -4(%ebp)
movl -4(%ebp), %eax
leave
ret
.size func, .-func
.globl main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
subl $20, %esp
movl $1, -12(%ebp)
movl $2, -8(%ebp)
movl $3, -4(%ebp)
movl -4(%ebp), %eax
movl %eax, (%esp)
call func
movl -8(%ebp), %eax
movl -12(%ebp), %edx
leal (%edx,%eax), %eax
movl %eax, -4(%ebp)
leave
ret
.size main, .-main
.ident "GCC: (GNU) 4.4.7 20120313 (Red Hat 4.4.7-11)"
.section .note.GNU-stack,"",@progbits
call調用完成保存ip寄存器以及jump的作用,進入func后保存之前的bp,設置新的棧頂和棧底(在leave時恢復)
。然后將臨時、本地數據保存以新的bp進行偏移保存,最后恢復ebp和esp,返回到caller繼續(xù)執(zhí)行。
call func作用
- push ip,保存下一條指令的地址
- jump func,修改ip跳轉到func執(zhí)行函數
func作用
- push ebp,保存bp
- mov esp,ebp,設置新的棧底。
- 以新的bp進行偏移,保存臨時、本地變量,完成函數功能
- leave(等價與mov ebp,esp;pop ebp)恢復esp和ebp
- ret 恢復ip,回到call的下一條指令繼續(xù)執(zhí)行。
2. Python棧切換
我們進行的切換方式與此類似,但是python的棧和c棧不同,python棧建立在虛擬機上。
總體上說,就是先進行c棧切換,關于ip設置跳轉到下條指令執(zhí)行(即執(zhí)行位置的切換,如何跳到函數位置開始執(zhí)行,如何從函數返回原來位置執(zhí)行),需要在python上實現top_frame的設置。
具體細節(jié)參考:
switch具體實現
幾個注意點:
- 導入greenlet會初始化一個main_greenlet,并設置current為main_greenlet
- greenlet運行結束,會返回到父greenlet執(zhí)行
from greenlet import greenlet
def func1(arg):
print (arg)
gr2.switch()
print ("func1 end")
def func2():
print ("fun2 come")
#設置parent為main_greenlet
gr1 = greenlet(func1)
gr2 = greenlet(func2)
value = gr1.switch("fun1 come")
print (value)
首先:
gr1.switch("func1")
會調用g_switch函數,其中target=gr1,args=('func1')
static PyObject *
g_switch(PyGreenlet* target, PyObject* args, PyObject* kwargs)
{
...
while (target) {
if (PyGreenlet_ACTIVE(target)) {
ts_target = target;
err = g_switchstack();
break;
}
if (!PyGreenlet_STARTED(target)) {
void* dummymarker;
ts_target = target;
err = g_initialstub(&dummymarker);
if (err == 1) {
continue; /* retry the switch */
}
break;
}
target = target->parent;
}
...
}
- gr1(new_greenlet),默認stack_start = NULL(沒有運行),stack_stop = NULL(沒有啟動),因而執(zhí)行g_initialstub()
- dummymarker設置為棧底
- 為什么要將dummymarker棧底設置于此處?
g_initialstub的棧中包含函數需要的參數等數據,然而&dummymarker的位置恰為g_initialstub棧的ebp。
g_initialstub
代碼已簡化
static int GREENLET_NOINLINE(g_initialstub)(void* mark))
{
...
/* 設置stack_stop,表明start該greenlet */
self->stack_start = NULL;
self->stack_stop = (char*) mark;
/* 設置target的上一個活動棧 */
/* Example:g1_greenlet.stack_prev=main_greenlet */
if (ts_current->stack_start == NULL) {
/* ts_current is dying */
self->stack_prev = ts_current->stack_prev;
}
else {
self->stack_prev = ts_current;
}
/* 核心代碼,進行棧切換 */
err = g_switchstack();
/* 標志greenlet正在運行,將要運行PyEval_CallObjectWithKeywords */
self->stack_start = (char*) 1; /* running
/* 設置當前運行參數為parent參數 */
self->run_info = green_statedict(self->parent);
?
/* 開始執(zhí)行函數 */
/* 注意:可能在該函數運行過程中,存在switch其他的greenlet,否則運行到函數結束 */
result = PyEval_CallObjectWithKeywords(
run, args, kwargs);
/* 標志函數結束 */
self->stack_start = NULL; /* dead */
/* 函數結束切換到parent運行 */
for (parent = self->parent; parent != NULL; parent = parent->parent) {
result = g_switch(parent, result, NULL);
}
- 設置當前greenlet的stack_prev為ts_current,即上一個正在運行的棧
- PyEval_CallObjectWithKeywords過程中可能會切換另一個greenlet,否則函數運行到結束
g_switchstack
static int g_switchstack(void)
{
int err;
{ /* save state */
/* 保存線程狀態(tài)或者說EIP */
PyGreenlet* current = ts_current;
PyThreadState* tstate = PyThreadState_GET();
current->recursion_depth = tstate->recursion_depth;
current->top_frame = tstate->frame;
current->exc_type = tstate->exc_type;
current->exc_value = tstate->exc_value;
current->exc_traceback = tstate->exc_traceback;
}
/* 匯編實現棧切換,分不同平臺 */
err = slp_switch();
if (err < 0) { /* error */
PyGreenlet* current = ts_current;
current->top_frame = NULL;
current->exc_type = NULL;
current->exc_value = NULL;
current->exc_traceback = NULL;
assert(ts_origin == NULL);
ts_target = NULL;
}
else {
/* 恢復線程狀態(tài),或者說EIP,即跳轉執(zhí)行位置 */
PyGreenlet* target = ts_target;
PyGreenlet* origin = ts_current;
PyThreadState* tstate = PyThreadState_GET();
tstate->recursion_depth = target->recursion_depth;
tstate->frame = target->top_frame;
target->top_frame = NULL;
tstate->exc_type = target->exc_type;
target->exc_type = NULL;
tstate->exc_value = target->exc_value;
target->exc_value = NULL;
tstate->exc_traceback = target->exc_traceback;
target->exc_traceback = NULL;
assert(ts_origin == NULL);
Py_INCREF(target);
ts_current = target;
ts_origin = origin;
ts_target = NULL;
}
return err;
}
- 保存線程狀態(tài),即EIP
- 進行C棧切換,匯編實現
- 恢復目標線程狀態(tài),即跳轉執(zhí)行位置
slp_switch(核心代碼)
static int
slp_switch(void)
{
/* 下面變量保存在棧(current)中 */
int err;
void* rbp;
void* rbx;
unsigned int csr;
unsigned short cw;
register long *stackref, stsizediff;
/* 這里save的是current線程的狀態(tài),變量保存在棧中 */
__asm__ volatile ("" : : : REGS_TO_SAVE);
__asm__ volatile ("fstcw %0" : "=m" (cw));
__asm__ volatile ("stmxcsr %0" : "=m" (csr));
__asm__ volatile ("movq %%rbp, %0" : "=m" (rbp));
__asm__ volatile ("movq %%rbx, %0" : "=m" (rbx));
__asm__ ("movq %%rsp, %0" : "=g" (stackref));
{
/* 保存當前線程的數據,包括上面的那些寄存器等等數據 */
/* 當為new_greenlet直接返回1,無棧可切換 */
SLP_SAVE_STATE(stackref, stsizediff);
/* 重要!current在此暫停,target從此處繼續(xù)之前的狀態(tài)之前 */
__asm__ volatile (
"addq %0, %%rsp\n"
"addq %0, %%rbp\n"
:
: "r" (stsizediff)
);
/* 恢復棧(target)中數據 */
SLP_RESTORE_STATE();
__asm__ volatile ("xorq %%rax, %%rax" : "=a" (err));
}
/* 恢復寄存器變量,這里恢復的是之前保存在target棧中的變量 */
/* 恢復了target的esp和ebp,因為變量的保存是以ebp進行偏移尋址中,所以當進行恢復時,進行相同偏移,但是因為ebp為已變?yōu)橹暗膖arget棧,因而恢復的寄存器也仍為之前的狀態(tài)。 */
__asm__ volatile ("movq %0, %%rbx" : : "m" (rbx));
__asm__ volatile ("movq %0, %%rbp" : : "m" (rbp));
__asm__ volatile ("ldmxcsr %0" : : "m" (csr));
__asm__ volatile ("fldcw %0" : : "m" (cw));
__asm__ volatile ("" : : : REGS_TO_SAVE);
return err;
}
- 很重要的一點,當從恢復ebp和esp開始,current暫停,target繼續(xù)之前運行,恢復之前數據,恢復的寄存器也仍為之前保存的狀態(tài),因為他們是基于ebp的偏移尋址,尋址方式不變,只受ebp的控制。