纯用户空间抢占式多线程的设计

晨曦之光 发布于 2012/04/10 14:59
阅读 317
收藏 0

纯用户空间的抢占式多线程库其实是很麻烦的一件事,在设计之前首先必须明白抢占式多线程的意义,其本质就是古老的unix多道程序设计,策略可以是分时的,也可以是其它任何的调度策略,不管什么策略,机制要素都是底层的OS内核和机器硬件提供的,对于x86上的linux来说,这些要素包括:分页机制--提供进程间相同虚拟地址不冲突的栈,线程间不同虚拟地址不冲突的栈;时钟中断以及任意中断机制--可以在不通知用户进程的情况下中断之,然后进行调度抉择,该机制是调度策略的前提;fork机制--启动新线程。只有完全模拟出以上等机制,多线程才是抢占式的。网上有篇文章用setjmp和longjmp实现了一个协作多线程,由于何时调度必须由线程自己决定,因此那不能算是抢占式的。由于x86上的linux内核的分层设计并没有提供下层对上层的调用,因此实现一个纯用户空间的抢占式多线程真的很麻烦。
     纯用户空间抢占式多线程的外部环境有两个要点:要点之一是抽象一台机器,该抽象的机器必须可以在进程外部将进程中断,有一种办法是向进程发信号;要点之二是必须能够得到进程当前的环境,比如所有寄存器,并且能保存这个环境。内部环境也有两个要点:其一是每个线程必须有一个属于自己的栈,由于这是纯用户空间的线程,因此最好自己用诸如malloc的方式动态分配;其二是每个线程必须可以自己启动。下面是MultiThread的部分代码:
//jmp_buf env[2];
//int idx[2];
void interrput_func (int sig)
{
    static int flag = 1;
    //idx[1] = setjmp(env[1]); //可惜setjmp只能保存当前栈的context,因此在此无法获得被信号中断之前的context,故而必须通过ptrace接口帮助。
    if (1 == flag) {
        flag ++;
        ...//创建一个新的堆栈,也就是重新设置esp寄存器,在新的堆栈上启动thread_func2
        thread_func2()
    }
}
void thread_func1 ()
{
    while (1) {
        printf("f1---/n");
    }
}
void thread_func2()
{
    while (1) {
        printf ("f2---/n");
    }
}
省略创建堆栈的代码,thread_func2和thread_func1必须在不同的堆栈上方可无错误地执行。
在一个父进程中fork-exec上述的程序MultiThread,然后用ptrace接口跟踪之,在发送SIGUSR信号给MultiThread并被父进程得知后,父进程交替使用PTRACE_GETREGS/PTRACE_SETREGS保存并设置上述程序的寄存器环境,如此就可以交替执行thread_func1和thread_func2了。
     上述代码中注释调用setjmp的语句,本来用setjmp/longjmp+signal可以很好的模拟操作系统的多线程,可是jmp_buf保存的context在调用函数返回后就会失效,而signal函数是在当前栈或者另分配的栈(使用sigaltstack)上执行的,无论哪种情况,最后都要调用sigreturn,因此在信号处理函数中的setjmp是无效的,setjmp只针对当前栈帧有效,这里的要点是,要想实现抢占式多线程,栈的切换是必然的,栈的切换不能影响寄存器环境的保存,因此必须使用ptrace等机制显式的设置进程的寄存器上下文,我们之所以还是使用了信号机制,那是因为信号机制可以中断进程并且通知ptrace进程,从而给ptrace进程修改MultiThread进程寄存器上下文从而模拟多线程的机会。另外,线程的启动也是一个要点,在一个执行绪的情况下,你几乎不可能在当前的栈帧中启动使用另一个栈的另一个线程,所有的基于冯诺依曼体系的机器本身都是单执行绪的,所谓的x86机器的多线程只是在进程这个层面的下层保留了一系列的上下文环境,然后不断切换它们从而模拟了多个线程,正如OS内核线程的创建及启动需要底层系统调用一样,用户空间的多线程创建及启动需要信号机制(使用信号仅仅是一个例子,也可以用别的),同样的理由,在冯氏机器上实现用户空间多线程必须借助别的执行绪,比如ptrace的帮助。
     如果setjmp可以得到被中断前的上下文,并且longjmp可以设置被中断后的上下文,并且不影响全局变量的话,正如kernel的context_switch一样,那么MultiThread的interrput_func就会成为:
static int flag = 0; //flag标识执行绪是信号处理进入的还是longjmp进入的。
void interrput_func (int sig)
{
    flag = 1;
    idx[1] = setjmp(env[1]);  //注释*
    if (1 == flag) { //如果是正规的信号处理则切换线程;
        flag = 0; //设置全局变量,因为下面的longjmp之后,执行绪将从注释*下面开始,由于已经切换了上下文,故到时将不再切换
        longjmp(env[0], idx[0]);
    }
    //否则信号返回,这个执行绪不是信号处理进入的,而是longjmp进入的。
}


原文链接:http://blog.csdn.net/dog250/article/details/5735512
加载中
返回顶部
顶部