一直以来,读过的代码和做过的项目都没有什么笔记,以至于很多学过的看过的,到后面居然记不起来,所以好记性不如烂笔头,好好总结,画图记录下来,总是有好处的,而且有很多东西也是没有去深入研究。
之前有幸在学习srs服务器中,接触到了state thread库,并且将srs改造成了支持多线程,多协程的版本,让srs不再只使用单一cpu,发挥多核优势的同时保持多协程的高效处理方式。
想想多协程在高并发中还是很好的处理方式,对一个网络请求来说,只要能将请求正确处理并返回,中间能不切换线程尽量不切换线程,这样能省下大量CPU时间,线程切换虽然不需要切换页表,但是线程间经常需要做事件等待,阻塞等消耗CPU的操作,因此引入了协程的概念,在应用层面实现类似CPU的多线程,主要用到了setjmp及longjmp系统调用。
setjmp及longjmp是C语言标准库中的函数,所以任何操作系统都支持。
int setjmp(jmp_buf envbuf):它将当前程序的栈内容保存到jmp_buf中,对于一段代码而言,如果不切换页表,那么能让它还原需要保存的变量主要应该是一些CPU寄存器,比如X86 CPU包括:CS,IP,EAX,EBX,ECX,EDX,SS,ESP,DS,ES,FS等【以后有时间再研究此结构体具体定义】。
定义:
typedef size_t jmp_buf[6];
int setjmp(size_t* buf);
void longjmp(size_t* buf, int);
setjmp源码:
setjmp:
mov eax, [esp+4]
mov [eax], ebx
mov [eax+4], ebp
mov [eax+8], esi
mov [eax+12], edi
mov [eax+16], esp
mov ecx, [esp]
mov [eax+20], ecx
mov eax, 0
ret
其实也比较简单,如下图:
setjmp调用后,按照C语言的生成方法,先push 参数,也就是jmp_buf(其实是个指针)到stack中,也就是图1中的P1;然后再执行jmp汇编指令,该指令会将eip存入栈中(由于是短跳转,所以CS不必要入栈),所以ESP+4指向的就是P1,也就是jmp_buf指针存放地址;
mov eax, jmp_buf
push eax
call $setjmp
add esp,$4 #还原栈
#上面这段应该是C语言调用方法
setjmp:
mov eax, [esp+4] #取到jmp_buf指针存到eax中
mov [eax], ebx #ebx存到jmp_buf[0]中(假设jmp_buf是DWORD*类型);
mov [eax+4], ebp #ebp存到jmp_buf[1]中
mov [eax+8], esi #esi存到jmp_buf[2]中
mov [eax+12], edi #edi存到jmp_buf[3]中
mov [eax+16], esp #esp存到jmp_buf[4]中
mov ecx, [esp] #EIP存到ECX中
mov [eax+20], ecx #EIP存到jmp_buf[5]中
mov eax, 0 #c语言规定函数返回值用eax返回,所以这里setjmp返回值为0
ret #ret后,会将ESP指向的内容存入EIP,ESP会加4,将EIP出栈,所以上面要提前保存EIP到jmp_buf[5]中;
所以setmp就把上述的寄存器保存到jmp_buf结构中了,然后也不管,在返回0的判断后,该干嘛继续干嘛。
再来看看longjmp(longjmp顾名思义,就是长跳转):
mov eax, status #这里应该是一个立即数地址,编译后的,这里只是示意
push eax
mov eax, jmp_buf
push eax
call $longjmp
#上面这段应该是C语言函数调用实现,忘记X86汇编了,有错误请指正
longjmp:
mov eax, [esp+8] #ESP指向EIP,ESP+4指向jmp_buf,ESP+8指向status
mov ecx, [esp+4] #取出jmp_buf,存入ecx
mov esp, [ecx+16] #将jmp_buf[4]还原到esp中,此时esp就指回了原来setjmp时的栈
mov ebx, [ecx] #将jmp_buf[0]还原到ebx中
mov ebp, [ecx+4] #将jmp_buf[1]还原到ebp中
mov esi, [ecx+8] #将jmp_buf[2]还原到esi中
mov edi, [ecx+12] #将jmp_buf[3]还原到edi中
mov edx, [ecx+20] #将jmp_buf[5]还原到edx中,也就是EIP存到edx中
mov [esp], edx #将edx放到esp指向的地方,也就是图1的中ESP指向的地址,然后将EIP放回到栈中,至此,还原到了setjmp调用结束ret那一刻的状态,只是eax是longjmp自己设定的;
ret #返回到setjmp(这就是setjmp的第二次返回了,其返回值就是longjmp设置的status)
今天先写到这里吧,主要把协程机制中最主要的setjmp及longjmp函数分析了下,从这里还可以看出,一个协程最好有自己的一个栈空间,至于具体如何设计后面再看。