模拟实现一个多线程环境

本文介绍了如何模拟实现一个多线程环境,包括线程及线程切换的概念、线程切换的实现,以及线程/进程管理类的接口和实现。通过C++代码展示了线程切换的具体步骤,同时提供了内存管理的简单实现。模拟系统适用于学习和实践,旨在帮助理解进程和线程调度的原理。
摘要由CSDN通过智能技术生成

引言

初学者或者一些有经验的开发人员,并不总是对于系统底层有清楚的了解。比如,进程(或线程)调度是如何实现的?往往只停留于模糊的认识。了解这些问题的最好途径是亲自实践。然而开发一个真实系统的门槛很高,而通过学习一个已有系统来了解也非易事,因为错综复杂的代码和关系有时把人搞糊涂了。于是学习者往往写一些模拟程序来努力表达系统是怎样运作的。不过,这些程序往往过于简单。于是“看看能否写一个模拟进程调度的软件”,从这个想法出发我尝试写一个接近真实的调度程序,因为进程或线程调度是现代操作系统的核心部分。经过一段时间的摸索一个调度程序写成了,同时写了一个简单的内存管理。接下来实现了一个模拟文件系统,差不多是按着0.11版的Linux文件系统实现的。我把这个模拟系统看作是学习和实践的一个场所,因此我主要用C++语言而不是C语言来编写,因为面向对象的方法(接口和类)更容易表达要被理解的东西。毕竟这是一个模拟程序,首要目标是帮助人学习和亲自实践,而不是以追求实现效率为先。目前这个模拟系统是在Windows平台上实现的,但它也可能在其它平台上实现。因为这个系统中只有少量与平台或硬件有关的代码,在不同平台上这部分的实现将有差异。

多线程/进程的模拟实现

线程及线程切换的概念

我们先来回顾下线程及其切换的概念。计算机通常按顺序一条一条地执行程序指令。多数处理器(CPU)通常内部借助一些寄存器来完成指令的执行,例如程序计数器(PC),指向下一条要执行的指令,其它是一些数据或状态寄存器等。另外,许多计算机支持使用堆栈(stack)来传递函数参数,存放局部变量等,因为寄存器的数量毕竟有限。通常用一个堆栈寄存器(SP)来指向当前所用的堆栈。当一段程序代码被执行的时候,处理器就处在一个状态中,这个状态可以用当前的程序计数器,堆栈指针,以及其它一些寄存器的集合来定义。如果我们把这个状态保存下来,转去执行其它代码,一段时间后再回过来,把先前保存的状态恢复,使各个寄存器恢复到以前保存的那个状态,继续执行下去,那么这一段程序的执行就好像没有被打断一样。这样一段程序的执行,就像一个独立的执行流或路径,这就是一个线程(thread)的含义了。系统中可以有许多线程,只要对每个线程都按照保存、恢复,恢复、保存来处理,就看起来像是同时各自独立运行。至于线程切换,就是把当前线程的执行状态保存起来,然后把另一个线程的已保存状态恢复过来,成为当前,这就完成切换了。

线程切换的实现

虽然我们大概明白线程及其切换的原理,但编写代码实现出来是最重要的。在寻求实现的过程中,我曾担心一件事:线程切换的实现,是否离不开特殊硬件指令的支持?恐怕是特权指令。我看到一篇网友的文章,他在用户程序中给出了从一个函数切换到另一个函数的例子,他的函数相当于线程函数。因此我参照他的代码在Windows平台上实现了。我用的开发工具为vs2008,为了完成线程切换,需要嵌入Intel汇编指令。

线程切换实现代码

void threadSwitch(int *pCurrentESP, int *pCurrentEIP, int nextESP, int nextEIP)
{
    __asm
    {
        push eax
        push ebx
        push ecx
        push edx
        push esi
        push edi
        push ebp
        //save ESP
        mov ebx,esp
        mov eax,[pCurrentESP]
        mov [eax],ebx
        //restore ESP
        mov eax,nextESP
        mov esp,eax
        //Save EIP
        lea ebx, _RECOVER_
        mov eax, [pCurrentEIP]
        mov [eax],ebx
        //Restore EIP
        mov eax, nextEIP
        push eax
        ret
_RECOVER_:
        pop ebp
        pop edi
        pop esi
        pop edx
        pop ecx
        pop ebx
        pop eax
    }
}



我们来分析这段代码如何完成切换的。pCurrentESP指向一个地址,用来存放当前线程的堆栈指针。pCurrentEIP也指向一个地址,用来存放当前线程的下一条指令地址。nextESP和nextEIP分别是将要切换过去的线程(简称下一个线程)的堆栈指针和下一条指令地址。切换的目标是把当前执行环境保存起来,并把下一个线程环境恢复成为当前。首先从push eax到push ebp是把一些通用寄存器保存到当前堆栈中 - 这个堆栈通常就属于这个线程。接下来保存当前线程的ESP,即堆栈指针,保存到pCurrentESP指向的地方 - 通常位于线程的内部数据结构中。后面又把当前线程的下一条指令地址保存起来,通常也是保存到线程内部数据结构中。请注意,下一条指令指向哪里?指向的是_RECOVER_标号处。这样,当以后再切回这个线程时,将执行_RECOVER_标号处代码,这部分代码所作的正是从堆栈中弹出和恢复那些通用寄存器的内容,这样就恢复到这个线程切换前的状态了。现在来看对下一个线程执行环境的恢复。首先是恢复堆栈指针,即把nextESP的内容赋给ESP寄存器,接下来恢复指令计数器EIP。由于EIP不能直接修改,这里利用堆栈并ret指令达到间接改变EIP的目的。这样就跳到下一个线程执行去了(如果此线程也调用这里的切换代码或者类似代码,可能马上就从堆栈中恢复那些通用寄存器,如前面所分析的),下一个线程成为新的当前线程。

我很高兴在上述线程切换的实现中,并未依赖于特殊硬件指令。这部分代码在所在平台下经过测试,证明可以工作。于是,我们在用户程序空间里实现了线程切换,因为模拟程序通常作为宿主机系统上的一个用户进程来运行。当前的实现,是Windows上的一个用户进程。

一个细节:上述切换代码,我定义成一个函数供调用,而不是使用宏(如linux的switch_to宏)。这个函数形式也是可行的。几个参数pCurrentESP,pCurrentEIP,nextESP,nextEIP是该函数的局部变量。C汇编代码对局部变量的访问是通过EBP寄存器。注意切换过程中虽然堆栈指针ESP被改变,但EBP并未改变,因此对这几个局部变量的访问总是有效的。

线程/进程管理类(接口)

线程切换实现之后,其它如线程的内部数据结构,线程调度等相对来说比较容易了。我打算把线程和进程的有关功能放到一个类中。为了清晰,先定义一个线程/进程的管理接口。我也决定在这个模拟系统中,以线程为基本调度单元。一个进程可以有一个或多个线程,其中有一个线程为主线程(主线程若结束,这个进程就要销毁了)。下面是线程/进程管理类接口,其中的接口函数都定义为纯虚函数。

线程/进程管理接口

typedef int (*THREAD_PROC)(void *parameter);

class ProcessManager
{
public:
//创建一个进程和其主线程。成功返回该进程的id(进程范围内唯一,大于0),失败返回0。
//主线程和其它线程的区别是,主线程运行结束,进程就终止,属于该进程的所有线程都释放。
virtual int createProcess(THREAD_PROC mainThreadProc, void *parameter, 
                int stackSizeInKB=1024, bool readyToRun=true) = 0;

//创建一个线程。成功返回该线程的id(线程范围内唯一,大于0),失败返回0。
//为了简化编程,约定线程id从1开始顺序编号(0为系统空闲线程所用)。
virtual int createThread(THREAD_PROC threadProc, void *parameter, 
                int stackSizeInKB=1024, bool readyToRun=true) = 0;
//进程/线程调度。
//对于合作式的线程调度,需要每个线程主动调用调度函数,让其它线程有机会执行。
virtual void schedule() = 0;
//获取当前进程的id。
virtual int getCurrentProcessId() = 0;
//获取当前线程的id。
virtual int getCurrentThreadId() = 0;
//获取进程的当前工作路径
virtual const char *getCurrentDir() = 0;
//设置进程的当前工作目录
virtual void setCurrentDir(const char *pathname) = 0;
//使当前线程睡眠指定的毫秒数
virtual void sleep(int msecs) = 0;
//获取锁,成功将拥有这个锁(如果之前已获取这个锁,将增加引用计数)。
//如果锁已经被其它线程拥有,调用线程将阻塞。
//一个锁可以被同一线程多次获取,但释放也要匹配相同的次数,否则其它线程不能获取它
virtual void lock(long lock) = 0;
//尝试获取锁,成功将拥有这个锁。如果锁已经被其它线程拥有,立即返回false
virtual bool trylock(long lock) = 0;
//释放锁
virtual void unlock(long lock) = 0;
};



这个线程/进程管理接口最主要的就是支持线程的创建,进程的创建,以及调度。在这里调度的基本含义是指线程调度,因为线程已作为基本调度单位。

对这个接口类需要作些说明。THREAD_PROC是线程函数的原型定义。schedule()是调度函数。为何要把调度函数公开出来呢?因为这个模拟系统目前只支持共享方式的调度。也就是说,这个系统中的每个线程需要主动放弃控制权,好让其它线程有机会运行。假如有个线程不释放控制权,从来不调用schedule(),它就一直独占系统,以致其它线程没有机会执行。此种情况类似一些早期的系统,例如早期Windows系统。如果要实现抢先式调度,就要接管中断和中断处理,因为中断能打断当前程序(线程)的执行,在中断处理函数中可以实现调度。模拟程序目前没有寻到一个方法来支持抢先式调度。createProcess是支持创建进程的接口函数,目前它是简单原始的。我们还未实现从磁盘可执行文件加载和创建进程。至于sleep和线程锁,放在这里主要是展示对于线程调度技术的运用。

线程/进程管理类的实现

我通常会先选择一种简单的实现。如果时间允许,或者有了想法,再去改进或写各种变化的实现。这样的做法是常见的。这个模拟系统,适合作为一个学习和练习的场所。

所用到的一些结构

//存放进程相关信息的结构
class ProcessInternalInfo
{
public:
    int processId;
    char currentDir[300];
    ProcessInternalInfo();
};

//存放线程相关信息的结构
class ThreadInternalInfo
{
public:
    //线程id及线程所属的进程id
    int processId;
    int threadId;
    //线程切换时对现场的保存和恢复,主要是栈顶指针(esp)和指令计数器(eip)
    int esp;
    int eip;
    //线程所使用的用户栈及其大小
    char *stackBase;
    int stackSize;    //字节
    //线程函数及其参数
    THREAD_PROC threadProc;
    void *parameter;
    //是否进程的主线程
    bool isMainThread;
    //错误代码
    int error;
    ThreadInternalInfo();
};

//内部堆栈,内部堆栈应该很小,因为只有thread wrapper function才用到
struct InternalStack
{
    char stack[4*1024];
};

//锁的实现结构
class Thread_Lock
{
public:
    long lock;
    int threadIndex;
    int refCount;
    Thread_Lock()
    {
        lock = 0;
        threadIndex = -1;
        refCount = 0;
    }
};

class Thread_Wait
{
public:
    int threadIndex;
    long lock;
    Thread_Wait()
    {
        threadIndex = -1;
        lock = 0;
    }
};



管理实现类的定义

class ProcessManagerImpl : public ProcessManager
{
public:
ProcessManagerImpl();
virtual ~ProcessManagerImpl();
//初始化
bool initManageData();

virtual int createProcess(THREAD_PROC mainThreadProc, void *parameter, 
            int stackSizeInKB=1024, bool readyToRun=true);
virtual int createThread(THREAD_PROC threadProc, void *parameter, 
            int stackSizeInKB=1024, bool readyToRun=true);
virtual void schedule();
virtual int getCurrentProcessId();
virtual int getCurrentThreadId();
virtual const char *getCurrentDir();
virtual void setCurrentDir(const char *pathname);
virtual void sleep(int msecs);
virtual void lock(long lock);
virtual bool trylock(long lock);
virtual void unlock(long lock);

//运行第一个进程,进程id为0。使用程序当前的堆栈。
int runProcess0(THREAD_PROC threadProc, void *parameter);
void setError(int error);
int getError();

protected:
//这里采用数组和索引管理所有进程/线程
ProcessInternalInfo *_pProcessInfo;
ThreadInternalInfo *_pThreadInfo;
//指向就绪线程队列
int *_pThreadReady;
//指向阻塞线程队列
int *_pThreadBlock;
//为每个线程分配一个内部栈。因为用户线程函数结束后,用户栈将被删除,
//thread wrapper function不能依靠用户栈工作
InternalStack *_pInternalStack;

protected:
//释放相关资源
void freeManageData();
//查找未用的进程槽,返回索引。未找到返回-1。
int find_free_process();
//查找未用的线程槽,返回索引。未找到返回-1。
int find_free_thread();
//加入到就绪线程队列
void addThreadReady(int threadIndex);
//加入到阻塞线程队列
void addThreadBlock(int threadIndex);
//创建线程
int create_thread(int processId, bool isMainThread, THREAD_PROC threadProc, 
                 void *parameter, int stackSizeInKB, bool readyToRun);
//thread wrapper function
//用户线程结束时要释放堆栈,并从调度队列中删除,所以需要一个对用户线程函数的包装
static void thread_wrapper_func(ProcessManagerImpl *pImpl, int threadIndex);
//从就绪线程队列中删除
void removeReadyThread(int threadIndex);
//从阻塞线程队列中删除
void removeBlockThread(int threadIndex);
//删除一个线程(重置对应的线程槽)
void removeThread(int threadIndex);
//删除一个进程(重置对应的进程槽)
void removeProcess(int processIndex);
//线程切换
void threadSwitch(int *pCurrentESP, int *pCurrentEIP, int nextESP, int nextEIP);
//系统死机 - 通常由于内部错误导致无法处理的情况,就进入这里
void system_dead();
//使当前线程睡眠,放入阻塞队列,然后切换到下一个线程
void block();
//唤醒一个线程,即从阻塞队列移除,放到就绪队列
void wakeUp(int threadIndex);

protected:
//对sleep的支持
//所有由于调用sleep函数导致睡眠的,超时时间记录在这里,单位毫秒。
int *_pThreadSleepTimeout;
//检查看是否有线程的睡眠时间到了,把所有这样的线程唤醒
void checkSleepTimeout();

//对锁的支持
Thread_Lock *_pThreadLock;    //存放锁的数组
int _threadLockCount;
Thread_Wait *_pThreadWait;    //等待锁的线程
int _threadWaitCount;
//查找锁,若找到返回在数组中的索引,未找到返回-1。
int findLock(long lock);
void removeLockByIndex(int lockIndex);
void addWait(int threadIndex, long lock);
//查找等待给定锁的线程,若找到,从等待中删除并返回线程号;若未找到返回-1。
int removeWait(long lock);

//对阻塞式文件读写的支持
//线程由于等待事件而睡眠的,等待的事件存放在下面的数组中
HANDLE *_pThreadEvent;
//线程等待事件,可能进入睡眠,直到事件变为信号状态而被唤醒
void waitEvent(HANDLE hEvent);
//检查所有的等待事件,把这样的线程唤醒
void checkThreadEvent();
void removeThreadEvent(int threadIndex);
friend class BlockedFileDisk;
};



在这个实现中,我使用了一个数组来存放所有的进程对象(_pProcess

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值