进程管理

本文详细介绍了操作系统中的进程管理,包括进程的初始化、切换和销毁。通过结构体TaskManage、Task、Register和ReeTrantLock展示了进程和资源的数据结构。在进程切换中利用时钟中断实现,而进程退出则通过exit系统调用进入等待队列,由清理进程负责回收资源。此外,还实现了一个可重入自旋锁以保证多进程同步和互斥。
摘要由CSDN通过智能技术生成

进程管理

1.设计

在KePOS系统中,涉及到多进程场景,设计进程的初始化,进程的切换,进程的销毁;并且设计了一个可重入自旋锁来实现进程的同步和互斥。

gShfhD.png

上图代表着每一个用户进程的线性地址空间,高地址为该进程的内核线性地址空间,低地址为该进程的用户线性地址空间。在每一个用户进程的页表中,都保存有完整的内核线性地址映射,和已经分配用户线性地址映射。地址0x7fffffffffff为该进程的栈基址(栈向低地址扩展)。系统调用时,仍然使用用户空间的栈,这样设计的原因是:我将系统调用也当作该进程发生的一种普通函数调用,只不过处理的代码发生在该进程的内核空间。

2.相关结构体

1.TaskManage

TaskManage结构体只有一个全局变量taskManage,用来管理所有的进程。该结构体管理3个进程链表(readys,running,waits),readys保存所有就绪态的进程,running指向当前运行中的进程,waits保存着所有的将要被销毁的进程,count用来分配新建的进程id。

struct TaskManage{
    struct Task * readys;
    struct Task * running;
    struct Task * waits;
    unsigned long count;
};
2.Task

Task结构体代表一个进程,cr3表示该进程的第一级页表的位置,pages表示该进程分配到的页面,structPage表示一个用户进程的栈所在的页面号,该属性对于内核进程无效,内核进程设计为直接将一块物理内存独占作为该内核进程的栈,所以不需要页面切换。state表示进程的状态,path为该进程在磁盘上的存输位置,window为该进程的窗口(如果进程没有窗口则为null),registers为该进程的寄存器值,用于进程切换时保护和恢复上下文。next和prev用于链接进程,使得进程保存在taskManage的链表中。

struct Task{
    unsigned long id;
    unsigned long cr3;
    struct PageBuffer pages;
    unsigned long structPage;
    unsigned long state;
    unsigned long path;
    struct Window * window;
    struct Register registers;
    struct Task * next;
    struct Task * prev;
} 
3.Register

该结构体表示在进程切换时,要保存的寄存器;在把A进程切换为B进程时,会把cpu寄存器的当前值保存到A进程的register结构体中,然后将进程B的register结构体中的值,恢复到cpu寄存器,这2步通过时钟中断来完成。

struct Register{
    unsigned long r15;
    unsigned long r14;
    unsigned long r13;
    unsigned long r12;
    unsigned long r11;
    unsigned long r10;
    unsigned long r9;
    unsigned long r8;
    unsigned long rbx;
    unsigned long rcx;
    unsigned long rdx;
    unsigned long rsi;
    unsigned long rdi;
    unsigned long rbp;
    unsigned long ds;
    unsigned long es;
    unsigned long rax;

    unsigned long rip;
    unsigned long cs;
    unsigned long rflagc;
    unsigned long rsp;
    unsigned long ss;
};
4.ReeTrantLock

在多进程环境中,需要有锁来实现进程间的同步和互斥。ReeTrantLock结构体为一个可重入的自旋锁,count表示锁状态,如果count>0表示有进程已经加锁,nowTask指向当前持有锁的进程,方便该进程可以重新获取锁。

struct ReeTrantLock{
    volatile unsigned long count;
    volatile struct Task * nowTask;
}

3.实现

1.用户进程初始化
//传入任务的第一个代码的线性地址
void initUserTask(void * userTask){
    //初始化一个task节点,用于保存进程的相关信息
    struct Task * newTask = (struct Task *)getMemoryBlock(sizeof(struct Task));
    initTaskNode(newTask,64);
    //分配新的页基地址
    unsigned long newCR3 = (struct Task *)getMemoryBlock(4096);
    newTask->cr3 = KernelVirAddressToPhyAddress(newCR3);

    //分配2页,并获取其物理地址
    unsigned long newPageID1 =  findFreeBlockInBitmap(memory.zone[RAM].bitmap, memory.zone[RAM].length, 1);
    modifyBitmap(memory.zone[RAM].bitmap, memory.zone[RAM].length, newPageID1);
    unsigned long phyAddress1 = getPageByPageID(newPageID1)->physicsAddress;
    set((unsigned int *)KernalPhyAddressToVirAddress(phyAddress1),(char)0x00,PAGE_2M_SIZE);
    setPageAttribute(getPageByPageID(newPageID1),1,USER);

    unsigned long newPageID2 =  findFreeBlockInBitmap(memory.zone[RAM].bitmap, memory.zone[RAM].length, 1);
    modifyBitmap(memory.zone[RAM].bitmap, memory.zone[RAM].length, newPageID2);
    unsigned long phyAddress2 = getPageByPageID(newPageID2)->physicsAddress;
    set((unsigned int *)KernalPhyAddressToVirAddress(phyAddress2),(char)0x00,PAGE_2M_SIZE);
    setPageAttribute(getPageByPageID(newPageID2),1,USER);

    //将新的页插入buffer。
    insertPagesBuffer(&newTask->pages,newPageID1);
    newTask->structPage = newPageID2;
   

    //设置页映射,首先分配2页,第一个页映射为线性地址0x0,第2个页映射为线性地址0x00007fffffe00000
    unsigned long rspVirAdd = 0x00007fffffffffff;
    unsigned long ripVirAdd = 0x00007fffffe00000;
    virPageTOPhyPage(ripVirAdd,phyAddress2,newTask->cr3,7);
    virPageTOPhyPage(0x0000000000000000,phyAddress1,newTask->cr3,7);

    //copy进程代码到进程的0x0线性地址,因为用户程序代码被loader保存在物理内存中
    copy((unsigned long)userTask,KernalPhyAddressToVirAddress(phyAddress1),200000);
    //设置该进程的初始化寄存器
    newTask->registers.cs = 0x23;
    newTask->registers.rip = 0x0000000000000000; //rip初始为线性地址空间0地址
    newTask->registers.ss = 0x1b;
    newTask->registers.ds = 0x1b;
    newTask->registers.es = 0x1b;
    newTask->registers.rsp = rspVirAdd;         //rsp初始为0x00007fffffffffff
    newTask->path = NULL;
    newTask->registers.rbp = newTask->registers.rsp;
    newTask->registers.rflagc |= (1UL << 9);

    newTask->id = taskManage.count;
    taskManage.count += 1;
    //初始化后,将该进程插入到taskManage的readys链表中
    insertTaskBehand(taskManage.readys->prev,newTask);
 
    newTask->state = READY;
    newTask->window = NULL;
    //初始化该进程的内核态页表-设置该进程的内核地址空间映射
    initKernelPage(newTask->cr3);

}
2.进程切换

通过中断/异常管理,知道在中断时,系统会自动保存rsp,rip,cs,ss寄存器,在中断处理函数入口处也会保存通用寄存器值,在中断返回出口时,首先会恢复通用寄存器值,最后iretq会弹出rsp,rip,cs,ss,然后会跳转到目标程序执行。利用该特征,通过时钟中断完成进程切换,每一个时间片为10ms。saveTaskRegisterTogStick(struct Task * task,unsigned long rsp)函数用于将中断入口压倒栈中的通用寄存器值保存到当前进程的task结构体的registers中。backTaskRegisterTogStick(nowTask,rsp)函数用于将当前进程task结构体registers中保存的值放到中断栈中。通过时钟中断返回时,恢复现场就可以跳转到新进程执行,切换后同时也要完成页表的切换。

//时钟中断处理函数
void timeHandle(unsigned long rsp){
    //.......
    //一个时间片10ms,进行一次任务切换
    changeTask(rsp);
   //.......
}
//任务切换,输入当前栈指针
void changeTask(unsigned long rsp){

    //如果没有就绪的任务,直接返回
    if((taskManage.readys->prev) == &NullTaskReady){
        return;
    }
    //如果有就绪,但是没有运行中的任务,则直接切换。
    if((taskManage.running->prev) == &NullTaskRunning){
        struct Task * nowTask = taskManage.readys->next;
        deleteTargetTask(nowTask);
        insertTaskBehand(taskManage.running->prev,nowTask);
        nowTask->state = RUNNING;
        backTaskRegisterTogStick(nowTask,rsp);
        //设置CR3为新进程的CR3,完成页表切换
        modifyCR3(nowTask->cr3);
    }else{
        //两个队列都不为空,把就绪队列的第一个加入running队列,把running的第一个移到就绪队列。
        struct Task * nowReadTask = taskManage.readys->next;
        struct Task * nowRunningTask = taskManage.running->next;
        deleteTargetTask(nowReadTask);
        deleteTargetTask(nowRunningTask);
        insertTaskBehand(taskManage.readys->prev,nowRunningTask);
        insertTaskBehand(taskManage.running->prev,nowReadTask);
        nowRunningTask->state = READY;
        nowReadTask->state = RUNNING;
        saveTaskRegisterTogStick(nowRunningTask,rsp);
        backTaskRegisterTogStick(nowReadTask,rsp);
        modifyCR3(nowReadTask->cr3);
    }
}
3.进程退出

首先内核进程初始化后,不会退出,所以只考虑用户进程的退出。

1.用户进程的执行流程:

下面这个_start在链接阶段,会被链接到0x0线性地址处,通过上面的说明,0x0线性地址是用户进程的第一条指令的地址。该程序功能时跳转到starttask函数执行。

_start:
	leaq starttask(%rip) ,%rax
    callq *%rax
    nop

反汇编代码

0000000000000000 <_start>:
 ;线性地址  机器码                 汇编代码
   0:	48 8d 05 e6 01 00 00 	lea    0x1e6(%rip),%rax        # 1ed <starttask>
   7:	ff d0                	callq  *%rax
   9:	90                   	nop

starttask函数的功能为跳转到main函数执行,并且在main执行完后,会执行一个exit系统调用退出。

void starttask(){
    __asm__ __volatile__   (
    "leaq main(%%rip) , %%rax \n\t"
    "callq *%%rax \n\t"
    :
    :
    :"memory"
    );
    //执行结束退出
    exit();
}

exit系统调用的最终处理函数如下:

该函数将当前进程插入到taskManage的waits链表,然后执行死循环等待进程切换,因此进程切换只在readys链表和running之间,所以该进程被切换后将不会在执行。内核中有一个清理进程,会监控waits链表,然后逐个清理waits链表里的进程。

//任务运行结束,任务退出
unsigned long sys_taskExit(){
    //将当前进程插入到等待队列,等待删除
    reetrantlock();
    struct Task * nowRunningTask = taskManage.running->next;
    //关中断,原子操作
    deleteTargetTask(nowRunningTask);

    insertTaskBehand(taskManage.waits->prev,nowRunningTask);
    //开中断,原子操作完成
    reetrantUnLock();

    //等待时钟中断切换到下一个任务
    while(True);
}

2.清理

//该进程负责清除wait队列中任务
void cleanTask(){
    while (True){
        while(taskManage.waits->prev != &NullTaskWait){
            exitNowtask(taskManage.waits->prev);
        }
    }
}
//清理进程
void exitNowtask(struct Task * nowRunningTask){
    reetrantlock();
    deleteTargetTask(nowRunningTask);
    //归还当前task获得的所有页面
    unsigned long pageId = nowRunningTask->structPage;
    modifyBitmap(memory.zone[RAM].bitmap, memory.zone[RAM].length, pageId);
    setPageAttribute(getPageByPageID(pageId),1,0);
    while(!isPagesEmpty(&nowRunningTask->pages)){
        pageId = deleteAndreturnPage(&nowRunningTask->pages);
        modifyBitmap(memory.zone[RAM].bitmap, memory.zone[RAM].length, pageId);
        setPageAttribute(getPageByPageID(pageId),1,0);
    }
    //归还CR3占用的空间
    backCR3page(nowRunningTask->cr3);
    //归还pagsbuffer
    backPagesBuffer(&nowRunningTask->pages);
    //归还该task结构体占用空间
    backMemoryBlock(nowRunningTask);
    //归还窗口占用的空间
    if(nowRunningTask->window->windata != NULL) deleteTaskWindow(nowRunningTask->window);
    reetrantUnLock();
}
4.可重入锁实现

因为进程只在中断时,才会切换;所以可以利用关中断来完成原子操作

1.加锁

//加锁
void reetrantlock(){
    //原子操作,关中断
    cli();
    //如果获得锁的进程不是当前进程,则判断锁是否可以被当前进程获取
    struct Task * noTask = taskManage.running->prev;
    int i;
    if(reeTrantLock.nowTask != noTask){
        //当前程序获取到锁
        if(reeTrantLock.count == 0){
            reeTrantLock.count ++;
            reeTrantLock.nowTask = noTask;
        }else{
            //当前线程获取锁失败,自旋。
            while(reeTrantLock.count != 0){
                sti();
                //等待一段时间在获取锁
                for(i = 0;i<1000;i++);
                cli();
            }
            //获得到锁
            reeTrantLock.count ++;
            reeTrantLock.nowTask = noTask;
        }
    }else{
        reeTrantLock.count ++;
    }
    //开中断
    sti();
}

2.解锁

//解锁
void reetrantUnLock(){
    //原子操作
    cli();
    //给当前锁的引用计数减去1
    reeTrantLock.count --;
    //如果当前锁的计数为0,该进程释放持有锁
    if(reeTrantLock.count == 0){
        reeTrantLock.nowTask = NULL;
    }
    sti();
}

4.详细代码

KeeProMise/KePOS: Design and implement your own operating system (github.com)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值