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

简介:
纯用户空间的抢占式多线程库其实是很麻烦的一件事,在设计之前首先必须明白抢占式多线程的意义,其本质就是古老的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进入的。

}



 本文转自 dog250 51CTO博客,原文链接:http://blog.51cto.com/dog250/1271792

相关文章
|
7月前
|
消息中间件 Java C++
"Java多线程基础-2:简介虚拟地址空间——保障进程间独立性的机制 "
如何保障进程之间这样的独立性?操作系统采用了“虚拟地址空间”的方式。
46 0
|
7月前
|
数据挖掘 API 数据处理
POSIX线程私有空间
POSIX线程私有空间
57 0
|
2月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
48 1
C++ 多线程之初识多线程
|
2月前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
20 3
|
2月前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
19 2
|
2月前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
30 2
|
2月前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
34 1