系统相关
首页 > 系统相关> > 操作系统:如何实现进程的等待与唤醒机制

操作系统:如何实现进程的等待与唤醒机制

作者:互联网

上节我们设计了Cosmos的进程调度器,但只有进程调度器是不够的,因为调度器它始终只是让一个进程让出CPU,切换到它选择的下一个进程上去。
结合进程生命周期,在多进程调度方面,还需要实现进程的等待与唤醒机制

进程的等待与唤醒

进程得不到所需的资源时就会进入等待状态,直到这种资源可用,才会被唤醒。那进程的等待与唤醒机制该如何设计?

进程等待结构

在实现进程的等待与唤醒机制之前,需要设计一种数据结构,用于挂载等待的经常,在唤醒的时候才可以找到那些等待的进程,代码如下:

typedef struct s_KWLST
{   
    spinlock_t wl_lock;  //自旋锁
    uint_t   wl_tdnr;    //等待进程的个数
    list_h_t wl_list;    //挂载等待进程的链表头
}kwlst_t;

这个结构在讲信号量时见过,因为它经常被包含在信号量等上层结构中,而信号量结构,通常用于保护访问受限的共享资源。

进程等待

让进程进入等待状态的机制,它也是一个函数。这个函数会设置进程状态为等待状态,让进程从调度系统数据结构中脱离,最后让进程加入到 kwlst_t 等待结构中,代码如下所示。

void krlsched_wait(kwlst_t *wlst)
{
    cpuflg_t cufg, tcufg;
    uint_t cpuid = hal_retn_cpuid();
    schdata_t *schdap = &osschedcls.scls_schda[cpuid];
    //获取当前正在运行的进程
    thread_t *tdp = krlsched_retn_currthread();
    uint_t pity = tdp->td_priority;
    krlspinlock_cli(&schdap->sda_lock, &cufg);
    krlspinlock_cli(&tdp->td_lock, &tcufg);
    tdp->td_stus = TDSTUS_WAIT;//设置进程状态为等待状态
    list_del(&tdp->td_list);//脱链
    krlspinunlock_sti(&tdp->td_lock, &tcufg);
    if (schdap->sda_thdlst[pity].tdl_curruntd == tdp)
    {
        schdap->sda_thdlst[pity].tdl_curruntd = NULL;
    }
    schdap->sda_thdlst[pity].tdl_nr--;
    krlspinunlock_sti(&schdap->sda_lock, &cufg);
    krlwlst_add_thread(wlst, tdp);//将进程加入等待结构中
    return;
}

有一点需要注意,这个函数使进程进入等待状态,而这个进程是当前正在运行的进程,而当前正在运行的进程正是调用这个函数的进程,所以一个进程想要进入等待状态,只要调用这个函数就好了。

进程唤醒

进程的唤醒则是进程等待的反向操作行为,即从等待数据结构中获取进程,然后设置进程的状态为运行状态,最后将这个进程加入到进程调度系统数据结构中。这个函数的代码如下所示。

void krlsched_up(kwlst_t *wlst)
{
    cpuflg_t cufg, tcufg;
    uint_t cpuid = hal_retn_cpuid();
    schdata_t *schdap = &osschedcls.scls_schda[cpuid];
    thread_t *tdp;
    uint_t pity;
    //取出等待数据结构第一个进程并从等待数据结构中删除
    tdp = krlwlst_del_thread(wlst);
    pity = tdp->td_priority;//获取进程的优先级
    krlspinlock_cli(&schdap->sda_lock, &cufg);
    krlspinlock_cli(&tdp->td_lock, &tcufg);
    tdp->td_stus = TDSTUS_RUN;//设置进程的状态为运行状态
    krlspinunlock_sti(&tdp->td_lock, &tcufg);
    list_add_tail(&tdp->td_list, &(schdap->sda_thdlst[pity].tdl_lsth));//加入进程优先级链表
    schdap->sda_thdlst[pity].tdl_nr++;
    krlspinunlock_sti(&schdap->sda_lock, &cufg);
    return;
}

空转进程

空转进程是系统下的第一个进程。空转进程是操作系统在没任何进程可以调度运行的时候,选择调度空转进程运行,可以说空转进程是进程调度器的最后的选择
注:这个最后的选择一定要有,现在几乎所有的操作系统,都有一个或者几个空转进程(多CPU的情况,每个CPU一个空转进程)

建立空转进程

我们的Cosmos的空转进程是个内核进程,按照常理,只要调用上节实现的建立进程的接口,创建一个黑河进程就好了。

但是我们的空转进程有点特殊,它是内核进程没错,但它不加入调度系统,而是一个专门的指针指向它。

由于空转进程是个独立的模块,需要建立一个新的C语言文件 Cosmos/kernel/krlcpuidle.c,代码如下:


thread_t *new_cpuidle_thread()
{

    thread_t *ret_td = NULL;
    bool_t acs = FALSE;
    adr_t krlstkadr = NULL;
    uint_t cpuid = hal_retn_cpuid();
    schdata_t *schdap = &osschedcls.scls_schda[cpuid];
    krlstkadr = krlnew(DAFT_TDKRLSTKSZ);//分配进程的内核栈
    if (krlstkadr == NULL)
    {
        return NULL;
    }
    //分配thread_t结构体变量
    ret_td = krlnew_thread_dsc();
    if (ret_td == NULL)
    {
        acs = krldelete(krlstkadr, DAFT_TDKRLSTKSZ);
        if (acs == FALSE)
        {
            return NULL;
        }
        return NULL;
    }
    //设置进程具有系统权限
    ret_td->td_privilege = PRILG_SYS;
    ret_td->td_priority = PRITY_MIN;
    //设置进程的内核栈顶和内核栈开始地址
    ret_td->td_krlstktop = krlstkadr + (adr_t)(DAFT_TDKRLSTKSZ - 1);
    ret_td->td_krlstkstart = krlstkadr;
    //初始化进程的内核栈
    krlthread_kernstack_init(ret_td, (void *)krlcpuidle_main, KMOD_EFLAGS);
    //设置调度系统数据结构的空转进程和当前进程为ret_td
    schdap->sda_cpuidle = ret_td;
    schdap->sda_currtd = ret_td;
    return ret_td;
}
//新建空转进程
void new_cpuidle()
{
    thread_t *thp = new_cpuidle_thread();//建立空转进程
    if (thp == NULL)
    {//失败则主动死机
        hal_sysdie("newcpuilde err");
    }
    kprint("CPUIDLETASK: %x\n", (uint_t)thp);
    return;
}

建立空转进程由 new_cpuidle 函数调用 new_cpuidle_thread 函数完成,new_cpuidle_thread 函数的操作和前面建立内核进程差不多,只不过在函数的最后,让调度系统数据结构的空转进程和当前进程的指针,指向了刚刚建立的进程。

上述代码中调用初始内核栈函数时,将 krlcpuidle_main 函数传了进去,这就是空转进程的主函数,下面我们来写好。

void krlcpuidle_main()
{
    uint_t i = 0;
    for (;; i++)
    {
        kprint("空转进程运行:%x\n", i);//打印
        krlschedul();//调度进程
    }
    return;
}

空转进程的主函数本质就是个死循环,在死循环中打印一行信息,然后进行进程调度,这个函数就是永无休止地执行这两个步骤。

空转进程运行

由于是第一进程,所以没法用调度器来调度它,我们得手动启动它,才可以运行。上节已经写了启动一个新建进程运行的函数,这里只需要调用它就好:

void krlcpuidle_start()
{
    uint_t cpuid = hal_retn_cpuid();
    schdata_t *schdap = &osschedcls.scls_schda[cpuid];
    //取得空转进程
    thread_t *tdp = schdap->sda_cpuidle;
    //设置空转进程的tss和R0特权级的栈
    tdp->td_context.ctx_nexttss = &x64tss[cpuid];
    tdp->td_context.ctx_nexttss->rsp0 = tdp->td_krlstktop;
    //设置空转进程的状态为运行状态
    tdp->td_stus = TDSTUS_RUN;
    //启动进程运行
    retnfrom_first_sched(tdp);
    return;
}

首先就是取出空转进程,然后设置一下机器上下文结构和运行状态,最后调用 retnfrom_first_sched 函数,恢复进程内核栈中的内容,让进程启动运行。

不过这还没完,我们应该把建立空转进程和启动空转进程运行函数封装起来,放在一个初始化空转进程的函数中,并在内核层初始化 init_krl 函数的最后调用,代码如下所示。

void init_krl()
{
    init_krlsched();//初始化进程调度器
    init_krlcpuidle();//初始化空转进程
    die(0);//防止init_krl函数返回
    return;
}
//初始化空转进程
void init_krlcpuidle()
{
    new_cpuidle();//建立空转进程
    krlcpuidle_start();//启动空转进程运行
    return;
}

效果图:
image

现在空转进程和调度器输出的信息在屏幕上交替滚动出现,这说明我们的空转进程和进程调度器都已经正常工作了。

多进程运行

虽然我们的空转进程和调度器已经正常工作了,但你可能心里会有疑问,我们系统中就一个空转进程,那怎么证明我们进程调度器是正常工作的呢?

现在想要看看多个进程会是什么情况,就需要建立多个进程。下面我们马上就来实现这个想法,代码如下。

void thread_a_main()//进程A主函数
{
    uint_t i = 0;
    for (;; i++) {
        kprint("进程A运行:%x\n", i);
        krlschedul();
    }
    return;
}
void thread_b_main()//进程B主函数
{
    uint_t i = 0;
    for (;; i++) {
        kprint("进程B运行:%x\n", i);
        krlschedul();
    }
    return;
}
void init_ab_thread()
{
    krlnew_thread((void*)thread_a_main, KERNTHREAD_FLG, 
                PRILG_SYS, PRITY_MIN, DAFT_TDUSRSTKSZ, DAFT_TDKRLSTKSZ);//建立进程A
    krlnew_thread((void*)thread_b_main, KERNTHREAD_FLG, 
                PRILG_SYS, PRITY_MIN, DAFT_TDUSRSTKSZ, DAFT_TDKRLSTKSZ);//建立进程B
    return;
}
void init_krlcpuidle()
{
    new_cpuidle();//建立空转进程
    init_ab_thread();//初始化建立A、B进程
    krlcpuidle_start();//开始运行空转进程
    return;
}

在 init_ab_thread 函数中建立两个内核进程,分别运行两个函数,这两个函数会打印信息,init_ab_thread 函数由 init_krlcpuidle 函数调用。这样在初始化空转进程的时候,就建立了进程 A 和进程 B。

效果如下:
image

进程 A 和进程 B 在调度器的调度下交替运行,而空转进程不再运行,这表明我们的多进程机制完全正确。

小结

本节实现了进程的等待与唤醒机制,然后建立了空转进程,最后对进程调度进行了测试。

你也许发现了,我们的进程中都调用了 krlschedul 函数,不调用它就是始终只有一个进程运行了,你在开发应用程序中,需要调用调度器主动让出 CPU 吗?
这是什么原因呢?这是因为我们的 Cosmos 没有定时器驱动,系统的 TICK 机制无法工作,一旦我们系统 TICK 机开始工作,就能控制进程运行了多长时间,然后强制调度进程。

标签:空转,操作系统,thread,调度,进程,td,tdp,唤醒
来源: https://www.cnblogs.com/whiteBear/p/16357948.html