操作系统:如何实现进程的等待与唤醒机制
上节我们设计了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;
}
效果图:
现在空转进程和调度器输出的信息在屏幕上交替滚动出现,这说明我们的空转进程和进程调度器都已经正常工作了。
多进程运行
虽然我们的空转进程和调度器已经正常工作了,但你可能心里会有疑问,我们系统中就一个空转进程,那怎么证明我们进程调度器是正常工作的呢?
现在想要看看多个进程会是什么情况,就需要建立多个进程。下面我们马上就来实现这个想法,代码如下。
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。
效果如下:
进程 A 和进程 B 在调度器的调度下交替运行,而空转进程不再运行,这表明我们的多进程机制完全正确。
小结
本节实现了进程的等待与唤醒机制,然后建立了空转进程,最后对进程调度进行了测试。
-
1、等待唤醒机制。为了让进程能进入等待状态随后又能在其它条件满足的情况下被唤醒,我们实现了进程等待和唤醒机制。
-
2、空转进程。是我们 Cosmos 系统下的第一个进程,它只干一件事情就是调用调度器函数调度进程,在系统中没有其它可以运行进程时,调度器又会调度空转进程,形成了一个闭环。
-
3、测试。为了验证我们的进程调度器是否是正常工作的,我们建立了两个进程,让它们运行,结果在屏幕上出现了它们交替输出的信息。这证明了我们的进程调度器是功能正常的。
你也许发现了,我们的进程中都调用了 krlschedul 函数,不调用它就是始终只有一个进程运行了,你在开发应用程序中,需要调用调度器主动让出 CPU 吗?
这是什么原因呢?这是因为我们的 Cosmos 没有定时器驱动,系统的 TICK 机制无法工作,一旦我们系统 TICK 机开始工作,就能控制进程运行了多长时间,然后强制调度进程。