winston 发表于 2012-1-16 10:20:39

用户级线程切换

实现线程库的一个核心问题是实现线程的切换。线程切换主要做了两件事:一是旧线程执行环境的保存,二是新线程执行环境的恢复。执行环境主要指寄存器,栈的切换也是通过寄存器的切换来完成的。
非抢占式用户级线程可以使用两类接口来实现: longjmp和setjmp; swapcontext,makecontext,setcontext. 在此不再赘述。
抢占式用户级线程切换
线程切换的时机。 切换分两种,一种是主动切换,一种是被动切换。主动切换是非抢占式的;被动切换发生在时间片用完之后,一个线程的时间片用完后就要强行切换到另外的线程。那么被动切换很自然的要发生在时钟中断里。在linux里面我们使用alarm信号。既然如此,赶快动手吧。
void timeHandler (int signo)
{
    thread_list *old;    if(signo != SIGALRM) return ;   
    signal(SIGALRM,timeHandler);   
    //! 如果有结束了的线程,释放该线程。
    ...   

    old=sys.current;
    if(old == 0){
      if(sys.threads){
            sys.current=sys.threads;
            longjmp(sys.current -> thread.buf,1 );
      }
    }else{
      sys.current=GetNext(sys.current)
      if(sys.current){
            if(!setjmp(old -> thread.buf,1)){
                longjmp(sys.current -> thread.buf,1 );
            }
      }
    }
}
我们会很悲观的发现,alarm信号只发生了一此。问题出在哪儿呢?原来linux的信号是不可重入的。进入信号处理函数之前,该信号被屏蔽,退出该信号处理函数之后该信号重新开放。而在信号处理函数中发生longjmp后,程序就跳转到其他线程,该信号处理函数不能退出。为了能再次进入信号处理函数,我们在切换之前就要重新开放该信号。我们可以使用siglongjmp/sigsetjmp代替longjmp/setjmp.
虽然该方法能够实现抢占功能,但总让人觉得不舒服。没有退出的信号处理函数让人如骨鲠在喉。有没有更优美的方法呢?
先来看一下信号处理的流程:以alarm信号为例。
1。 alarm信号到达,执行权由用户空间进入内核空间,栈自动切换到内核栈(内核栈指针存放在TSS结构的ESP0中)。同时用户空间的执行环境存放到内核栈中。
2。 内核进行一些信号相关工作。 最后发现我们注册了alarm信号的处理函数,然后建立执行信号handler的栈环境(通常在用户栈栈顶),将已经保存在内核栈中的用户执行环境复制到信号处理函数栈。 然后通过reti从内核返回到用户空间,恢复用户空间执行环境,执行信号处理函数。
3。 执行完信号处理函数后,再次进入内核,注意此次进入内核后,内核栈会再次自动保存用户空间执行环境,但这不是我们需要的。所以内核会将信号处理函数栈中的用户执行环境复制回内核栈。
4。再次从内核空间切换到用户空间,用户执行环境自动恢复到alarm信号到达时的执行状态。
分析后我们会发现,当我们执行信号处理函数时,栈顶是保存了用户执行环境的,通过更换这个执行环境,就可以达到用户态线程切换的目的。
#include <sys/ucontext.h>
# if __WORDSIZE == 64
#define OFFSET_TO_SIGCONTEXT 7
# else
#define OFFSET_TO_SIGCONTEXT 3
# endif

void timeHandler (int signo)
{
    unsigned int i, j;
    sigcontext* psig_context;      
    if(signo != SIGALRM) return ;
    signal(SIGALRM,timeHandler);
    __asm( "lea (%%" bp_register ", %1), %0":"=r"(psig_context): "r"(OFFSET_TO_SIGCONTEXT* sizeof(ptr_size*)));
    schedule(psig_context);
}
用户执行环境用sigcontext结构体来描述, 在64位系统中,该结构体位于 RBP + 7*8 位置处; 在32位系统中位于EBP+3*4位置处。 __asm( "lea (%%" bp_register ", %1), %0":"=r"(psig_context): "r"(OFFSET_TO_SIGCONTEXT* sizeof(ptr_size*))); 用于获得该结构体指针,然后就可以传到 调度函数用户线程切换了。
查看源代码


Vimium has been updated to 1.30.x






页: [1]
查看完整版本: 用户级线程切换