【翻译】Xv6 book Chapter 7:Multithreading

1 Scheduling

几乎所有OS上运行的进程数都大于CPU的核数,所以对CPU资源的时分计划是有必要的。在理想条件下,这种分享应当对用户是透明的。一个普遍的做法是通过多路复用的方法给每个进程提供一个假象:他们都各自拥有属于自己的虚拟CPU。本章揭示了xv6如何实现这种多路复用。

1.1 Multiplexing

Xv6通过在两种情况下去切换CPU上运行的进程来实现多路复用。第一种情况,当进程在等待设备或者管道IO时,或者等待子进程退出时,或者在等待 sleep系统调用结束时,xv6的 sleepwakeup 机制会切换进程。第二种情况,xv6会周期性地切换进程。这种多路复用会给进程创造一种假象,每个进程都有属于自己的CPU,就像是xv6中使用内存分配器和页表来实现的假象,每个进程都拥有他们自己的内存。

实现多路复用有一些挑战。第一,如何从一个进程切换到另一个进程?虽然上下文切换的想法很简单,但是它的实现是xv6中最难懂的部分。第二,如何在用户感知透明的情况下强制切换进程?Xv6使用了时钟中断驱动上下文切换的标准技术。第三,很多CPU可能会并发地切换进程,所以锁对于避免竞争非常必要。第四,一个进程的内存和其他资源必须要在进程还存在的时候释放,但是进程不能通过自己做完这些事,因为(举例)进程在使用内核栈的时候不能释放掉它。第五,每个CPU必须记住自己正在执行的是哪个进程。最后,sleepwakeup一个进程放弃CPU,并且sleep等待一个事件,并允许另外的进程唤醒。需要去注意会导致唤醒通知的丢失的竞争。Xv6尽量通过简单的方法去解决这些问题,但是最后的code还是有些棘手。

1.2 Code:Context switching

图7.1描述了进程切换的相关步骤:一个用户与内核的转换(系统调用或者中断),转换到旧进程的内核线程(所以是以线程为单位?),一个到当前CPU的调度线程的上下文切换,一个到新进程的内核线程的上下文切换,和一个返回用户态的trap调用。Xv6的每个CPU都有一个专用的线程(保存寄存器和栈),因为scheduler在一个旧进程的内核栈上执行是非常不安全的:某些其他core可能会唤醒旧进程并执行,但是两个core同时在相同的stack上面执行会是一个灾难。在本节我们将会测试在内核线程和scheduler线程间的切换机制。

线程间切换会涉及到保存旧线程的CPU寄存器恢复新线程之前保存的寄存器。栈指针和程序计数器的保存与恢复意味着切换栈以及切换执行的代码位置。

函数swtch会在内核线程切换时保存和恢复。swtch不关心线程,它只是保存和恢复上下文。当一个进程需要放弃CPU了,这个进程的内核栈会调用swtch去保存上下文并转到scheduler的上下文。每个上下文都被包含在 struct context,被包含在进程的struct proc和CPU的 struct cpu中。swtch包含两个参数,struct context * old 和 struct context * new。它把目前的寄存器内容保存在old中,把new中的内容恢复到寄存器中。

让我们来看看进程通过 swtch切换到scheduler的过程。我们在第四章学到过,usertrap有可能会调用yieldyield会调用sched,这个函数会调用swtch去保存上下文到 p->context,并切换scheduler的上下文到cpu->scheduler

Swtch 只保存callee-saved 寄存器,caller-saved寄存器通过调用C代码被保存在栈上面。Swtch知道每个寄存器在struct context上面的偏移。这不会保存程序计数器(那pc怎么恢复?从内核返回至用户态时,保存在trapframe上面的pc会复位)。而且swtch保存 ra寄存器,包含swtch被调用的地址。现在swtch从之前的上下文中恢复了寄存器。当swtch返回,他会返回到ra寄存器保存地址的指令位置,即之前该线程调用swtch的位置。而且它会返回到新线程的内核栈上面。

在例子中,sched调用swtch切换到 cpu->scheduler。scheduler通过调用swtch保存了上下文。当swtch返回,它不会返回到sched,而是scheduler,而且栈指针指向目前cpu的scheduler的栈上面。

1.3 Code:Scheduling

上一小节关注了有关于swtch的底层细节。现在我们去关注通过scheduler切换进程。scheduler以一种特别的线程形式存在,每个都执行scheduler函数。这个函数决定了下一个执行的进程。一个想要放弃CPU的进程必须要获得他自己的锁 p->lock,释放所有其他持有的锁,更新自己的状态(p->state),然后调用sched。Yeild旧遵循这个逻辑,还有我们之后需要讨论的sleepexitSched会两次确认状态:因为锁被持有,中断应当是被禁止的。最后,sched调用swtch去保存目前的上下文到p->context中,并且从cpu->scheduler中恢复scheduler的上下文。Swtch返回到scheduler的栈上面。scheduler会进行for loop,知道找到一个进程执行。

我们在上面已经谈及xv6在调用swtch中会持有p->lock:swtch的调用者必须已经持有了锁,并且这个锁的控制传递到了转向的代码上面。这个逻辑是在锁中是不同寻常的,通常申请所的线程会负责释放锁,这回使得很容易保证正确性。**yield申请的锁在哪里释放?**对于上下文切换来说,非常有必要去打破这个传统,因为p->lock保护了在swtch执行过程中不正确的进程state和context域。举个例子,如果p->lock在swtch中没有被持有:一个不同的CPU可能会决定在yield设置状态为 RUNNABLE 之后执行这个进程,但是是在swtch停止使用它的内核栈之前。这会导致两个CPU运行在相同的栈上面。

一个内核线程进程会在sched调用中放弃它的CPU,并且切换到scheduler线程,然后再切换到某个之前调用过sched的内核线程。因此,如果打印xv6切换线程的行号,会是以下的形式: (kernel/proc.c:475), (kernel/proc.c:509) , (kernel/proc.c:475) , (kernel/proc.c:509) …的形式。在两个线程之间进行这种程式化切换的过程有时被称为协程:再这个例子中,schedscheduler互为携程。

有一种情况下scheduler对swtch的调用不会在sched中结束。当一个进程第一次被调度,他会从forkret开始。Faorkret会释放p->lock,否则,新的进程会在usertrapret中开始。(????)

scheduler执行一个简单的循环,找到一个进程去执行,并执行,直到它放弃。scheduler在进程表上面找一个runnable的进程。一旦找到了,就设置CPU现在的进程变量c-proc,并标记该进程为RUNNING,然后调用swtch开始执行该进程。

一种思考调度代码的方法如下,强制每个进程满足一些要求(不变性),并且在这些不变量not true的情况下持有p->lock。一个要求是如果进程是RUNNING状态,一个时钟中断的yield必须能够安全的切换出去;这意味着CPU寄存器必须持有该进程的所有寄存器值(swtch还没有把他们移到context中)且c->proc必须指向该进程。另一个要求是,如果一个进程是RUNNABLE,那么空闲的CPU scheduler可以安全地执行它;这一位置p->context 必须持有这个进程的寄存器值(不是真正的寄存器),没有其他CPU执行在这个进程的内核栈上面,没有其他的CPU的c->proc指向该进程。注意,当p->lock被持有时,这些属性通常不为真。

维持上述的不变性是xv6在一个线程中申请p->lock,并且在另一个线程中释放的理由。举例,在yield中申请,在scheduler中释放。一旦yield开始修改正在执行进程的状态为RUNNABLE,这个锁必须被持有到不变性被恢复:最在正确的时放点是在scheduler(运行在它自己的栈上面)清楚了c->proc。类似的,一旦scheduler开始将一个进程从RUNNABLE到RUNNING,锁只有到内核线程完全开始执行才能释放(在yield的例子中,需要在swtch之后)

p->lock同样保护其他东西:waitexit之间的相互影响,避免唤醒丢失的机制,避免一个正在exit的进程与其他读写其状态的竞争(exit系统调用会查看p->pid和修改p->killed)。为了简洁和效率,值得去思考,拆分有关p->lock的函数。

1.4 Code:mycpu and myproc

Xv6 经常需要一个指向当前进程的 proc结构体的指针。在单处理器的机器上面可以有一个全局变量来指明当前的proc。但这在多核机上面不适用,因为每个核都执行一个不同的进程。处理方法是:每个核拥有自己的一系列寄存器,我们可以使用其中一个寄存器来帮助找到每个核的信息。

Xv6为每个CPU维护了一个struct cpu,记录了当前运行的进程,为scheduler线程保存的寄存器内容,以及管理中断禁用所需的嵌套自旋锁的数量。函数mycpu返回一个指向当前CPU的struct cpu的指针。RISC-V通过给每一个CPU一个hartid来编号。Xv6确保在kernel中,每个CPU的hartid被存在了CPU的tp 寄存器中。这使得mycpu可以用tp来找到正确的cpu struct。

确保一个CPU的tp寄存器一直持有CPU的hartid是有点负载的。在machine mode时,mstart在CPU启动过程中设置tp寄存器。usertrapre会保存tp寄存器的内容到trampoline page中,因为这个用户进程可能会修改tp寄存器。最后,uservec会在从用户控件到内核空间的过程中恢复保存的tp值。编译器保证了永远不去使用tp寄存器。如果RISC-V允许xv6直接读取当前的hartid将会很方便,但是这仅被允许在machine mode下执行,而不是supervisor mode。

cpuidmycpu的返回值的脆弱的:如果发生时钟中断,当前线程放弃当前cpu,转到其他的cpu上面,那么先前的返回值不再是正确的。为了避免这个问题,xv6要求调用者禁用中断,直到他们完成使用返回值 struct cpu。

函数myproc返回struct proc的指针,该指针指向当前cpu执行的进程。myproc会禁止中断,并调用mycpu,得到目前的进程指针(c->proc),然后启用中断。即使中断启动,该返回值也 是有效的:如果一个时钟中断移动调用线程到另一个CPU,它的返回指针也是相同的。

1.5 Sleep and wakeup

调度和锁帮助隐藏了进程切换的存在,但是目前我们还没有一个抽象来帮助进程主动交互。有很多机制被创造出来解决这个问题。Xv6使用一种叫做sleep和wakeup的机制,这允许一个进程陷入睡眠并等待一个事件,一旦该事件发生另一个进程就去唤醒它。这经常被叫做 sequence coordination 或者 conditional synchronization机制。

为了说明,我们先考虑一个叫做信号量的同步机制,这是在消费者和生成者间调节的机制。一个信号量维护了一个计数,并提供两种操作。”V“操作(供生产者使用)增加计数。”P“操作(供消费者使用)等待直到计数非零,并减小计数。如果只有一个消费者线程、一个生产者线程,并且他们执行在不同的CPU上面,编译器不过分优化,以下的执行会是正确的。

代码就不复制了~

以上部分的实现是非常昂贵的。如果生产者很少操作,那么消费者会陷入自旋。消费者的CPU可以找到更有价值的工作,而不是进入一个忙等待。避免忙等待需要一种方式让消费者放弃CPU,并且只有当”V“操作增加计数之后继续。

这会向前进一步,但是我们会看到这是不够的。让我们想象一对sleepwakeup的调用,如下运行。Sleep(chan)会sleep在一个随机值chan上,叫做wait channel。Sleep使得调用进程进入sleep,释放CPU。Wakeup(chan)会唤醒在该chan上面沉睡的所有进程,使得他们的sleep调用返回。如果没有进程等待在该chan上面,wakeup不做任何事情。我们可以通过sleepwakeup来改变信号量的实现。

代码

”P“操作会放弃CPU,然不是进入自旋。然而,这被证明不能避免出现丢失wake-up问题。假设”P“操作发现s->count == 0 在行212上。当”P“操作在行212和行213之间时,”V“操作在另一个CPU上被执行,并增加了计数,调用了wakeup,但是此时没有进程陷入沉睡,所以不做任何事情。此时”P“执行行213,陷入盛水。这回导致一个问题:”P“沉睡等待一个已经发生的”V“操作唤醒。取费这个进程再次调用”V“,否则这个消费者会永久的陷入沉睡,即使计数不为零。

问题的根源是”P“只会在s->count == 0沉睡的不变性被一个运行在错误时间的”V“操作侵犯了。一种不正确的保护不变性的方式是移动”P“中请求锁的位置,因此检查计数核调用sleep是原子的:

代码

设计者可能希望这个版本的”P“会避免丢失唤醒,因为锁避免了”V“操作执行313和行314.它确实做到了,但是这也产生了思索:”P“在陈水中持有了锁,所以”V“永远会阻塞在申请锁中。

我们将会通过改变sleep的接口来修改之前的方案:调用者必须传递一个状态条件锁给sleep,所以它可以在调用进程被标记为沉睡状态之后释放锁。这个锁会强制一个并发的”V“等待到”P“进入沉睡,所以wakeup将会找到沉睡的消费者,并唤醒它。一旦消费者再次被唤醒sleep会在返回前重新申请锁。新的正确的sleep/wakeuo方案按照以下代码被使用:

代码

”P“持有锁可以避免”V“在”P“检查计数和调用sleep之间尝试唤醒它。然而,记住我们需要sleep原子化地释放锁,并且使得消费者进程进入沉睡。

1.6 Code: Sleep and wakeup

让我们来看sleepwakeup地实现。sleep地基本思路是将当前地进程标记为SLEEPING然后调用shced放弃CPU;wakeup寻找一个在给定channel上面沉睡的进程,并调集为RUNNABLE。sleepwakeup的调用者可以使用任何数当作channel。Xv6经常使用内核数据结构的地址。

sleep申请p->lock。现在正在进入sleep的进程同时持有p->lock和lk。持有lk对于掉者非常重要:这却汇报了没有其他的进程能够调用wakeup(chan)现在sleep持有p->lock,那么释放lk就是安全的:某些进程可能会去调用wakeup(chan),但是wakeup会等待至申请到p->lock,即直到sleep已经使得进程进入沉睡,使得wakeup不会错过sleep

这里会有一个小的问题:如果lk == p->lock,那么sleep将会死锁。但是如果进程如果进程在调用sleep的时候已经持有了p->lock,那么就不需要做其他事情来避免发生唤醒丢失。这种情况会出现在wait调用sleep的时候。

既然sleep只持有了p->lock,它就可以通过sleep channel使得进程进入沉睡,改变进程的状态为SLEEPING,并且调用sched。稍后就会明白为什么在进程被标记为sleep之前(by scheduler)不释放p->锁是至关重要的。

在某个时间点,一个进程会申请条件锁,然后设置状态为沉睡进程正在等待,然后调用wakeup(chan)。在持有条件锁的情况下调用wakeup非常重要。wakeup会在进程表上面循环。它会对每个进程表上的进程申请锁,因为它可能会修改进程的状态,而且p->lock确保了sleepwakeup不会错过对方。当wakeup发现一个进程在对应的chan上面,且是沉睡状态,那么它会将该进程的状态设置为RUNNABLE。等到下次scheduler执行,它会发现该进程可以为执行。

为什么sleepwakeup的锁规则可以确保沉睡的进程不会丢失唤醒?从检查条件之前到将进程标记为SLEEPING之间,沉睡进程始终都持有条件锁或者p->lock或者两者都有。调用wakeup的进程在循环中同时持有两种锁。因此唤醒者需要在消费者线程检查其条件前确保条件为真;或者唤醒者的wakeup需要在沉睡线程被标记为SLEEPING之后进行严格检查。然后wakeup将会找到沉睡线程,并唤醒它(除非有其他线程已经唤醒了它)。

有时会有多个进程沉睡在同一个channel的情况。举例,多个进程从一个pipe上面读。一个wakeup将会把它们全部唤醒。其中一个会第一个执行,并且在sleep中重新获得锁,并且读完pipe中的素有数据。另外的进程会发现自己虽然被唤醒了,但是pipe里面没有数据。从它们的角度来看这次唤醒是虚假的,他们必须重新陷入沉睡。因此,sleep经常会在循环中被调用来检查条件。

1.7 Code: Pipes

xv6中一个更加复杂的使用了sleepwakeup来同步生产者和消费者的例子是pipe的实现。我们在第一张见过了pipe的接口:从pipe一段中写入的字节流会被拷贝到内核的缓冲区,并且可以从pipe的另一端读取。下面的章节将会解释文件描述符对pipe的支持,但是我们可以先关注于 pipewritepiperead的实现。

每个pipe都用一个struct pipe 来表示,其包含了一个lock和data buffer。nread和nwrite分别表示了写入和读取的字节数。缓冲区是循环的(类似于循环队列):buf[PIPESIZE-1]的下一个字节是buf[0]。但是计数是不循环的。这个设定使得满的缓冲区( nwrite ==
nread+PIPESIZE)和一个空的缓冲区(nwrite == nread)不相同,但是这也意味着对缓冲区的索引必须使用buf[nread % PIPESIZE]的形式,而不是buf[nread](nwrite也是一样)。

假设pipereadpipewrite同时在两个CPU上发生。pipewrite申请了pipe的锁,这会保护计数、数据以及相关的不变性。piperead然后也尝试去申请锁,但是会失败。它会在acquire自旋等待锁释放。当piperead在等待的时候,pipewrite会循环地写入字节,直到pipe满了。此时,pipewrite会调用wakeup去唤醒沉睡的reader,通知他们目前pipe有数据可以被读取。然后再沉睡在&pi->nwrite 上等待reader读走字节。Sleep释放pi->lock,作为让pipewrite进程进入睡眠的一部分。

既然pi->lock目前可以获得,那么piperead会申请到它,并进入临界区:它发现 pi->nread != pi->nwrite (因为pi->nwrite == pi->nread+PIPESIZE,所以pipewrite陷入沉睡),所以它进入for循环,把管道中的数据拷贝出去,并增加nread的计数。现在可以写入字节了,所以piperead会在返回之前调用wakeup去唤醒writer。wakeup发现一个进程沉睡在&pi->nwrite,该进程执行了pipewrite但是因为buffer满了所以陷入沉睡。该进程被标记为RUNNABLE。

pipe的代码把reader和writer的sleep channel分开了( pi->nread , pi->nwrite),可使得系统在多读者、多写者沉睡于同一个pipe的时候更加地高效。pipe的代码中通过魂环去检查沉睡条件;如果有多读者和写者,除了第一个被唤醒的线程其他都会发现条件非真,并重新陷入沉睡。

1.8 Code: Wait, exit, and kill

sleepwakeup可以用在很多的等待过程中。在第一张介绍过的一个有趣的例子是子进程的exit和父进程的wait。在子进程死亡的时候,父进程可能以及通过wait陷入了沉睡,或者在做其他事情;在后者的情况下,之后调用的wait必须要观察到子进程的死亡,可能会远迟于子进程exit。Xv6通过wait观察到子进程因调用exit进入ZOMBIE状态,记录子进程的消亡,并修改状态为UNUSED,复制子进程的退出状态,并返回子进程的id给父进程。如果父进程在子进程之前exit,那么这个回收的责任会交给init进程,init进程会一直调用wait;因此所有的子进程都会被回收。主要的实现困难是在父子进程之间的竞争和死锁的可能性。

wait使用调用进程的p->lock作为条件锁,来避免丢失唤醒,并且其在开头就申请锁。然后开始扫描进程表。如果找到了子进程的状态为ZOMBIE,它会释放掉子进程的资源和它的proc结构体,复制子进程的退出状态给wait并i企鹅返回子进程的pid。如果wait发现没有子进程退出,它会调用sleep,等待某个子进程退出,然后重新扫描。此处在sleep中释放的条件锁就是等待线程的p->lock,即上述提到的特殊情况。注意wait会经常持有两个锁;在申请子进程的锁之前会申请自己的锁;所以xv6中必须遵循相同的加锁顺序(父进程,然后子进程)来避免死锁。

wait通过查看每个进程的np->parent来寻找子进程。它使用mp->parent的时候是不持有其锁的(np->lock),因为这回违反上述对于共享变量的持有顺序的规则。有可能np是当前进程的父进程,而在这种情况下再去获得np->lock会导致死锁,因为这违反了上述的规则。不加锁检测np->parent似乎是安全的;一个进程的parent域只能被其父进程修改,所以如果np->parent == p 为真,那么除非是当前的进程修改它,否则都不能修改。

exit记录了退出的状态,释放了资源,委托释放子进程的任务给init进程,唤醒在等待的父进程,标记调用的进程为ZOMBIE,永久的放弃CPU。最后的顺序是有点复杂的。当设置自己的状态为ZOMBIE并唤醒父进程的时候,退出的进程必须持有父进程的锁,因为父进程的锁会避免wait中的唤醒丢失。子进程必须持有自己的p->lock,因为父进程可能会在其仍在运行的时候,发现其状态为ZOMBIE,并且释放它。锁的申请顺序对于避免死锁非常重要:因为wait在申请子进程的锁之前申请父进程的锁,exit必须以同样顺序执行。

exit调用特别的唤醒函数,wakeup1,它只会唤醒在wait状态下的父进程。在子进程修改自己的状态为ZOMBIE之前唤醒父进程看起来是不正确的,但是其实是安全的:即使wakeup1可能会导致父进程重新执行,wait中的循环也不会查看子进程,直到子进程的p->lock被scheduler释放,所以wait不能查看子进,直到子进程把它的状态设置为ZOMBIE。

exit允许进程终止自己,kill允许一个进程请求另一个进程终止。对于kill来说直接摧毁一个进程会非常复杂,因为受害者可能会执行在另一个CPU上面,可能正在修改谋者敏感的内核数据结构。因此kill只做一点点事情:只是设置受害者进程的p->killed,如果该进程在盛水状态,唤醒它。最后受害者进程会进入或者离开内核,如果p->killed为真,usertrap会调用exit。如果受害者进程运行在用户空间,那么他会很快进入内核通过系统调用或者时钟中断。

如果受害者进程在sleepkill调用的wakeup会唤醒它。这可能会是位线的,因为它等待的条件可能是非真的。然而,xv6中调用sleep都是包装于while中的,这会重新检验条件。谋者对sleep的调用也会在while中检验p->killed,,并且如果为真,则丢弃目前的任务。只有当这种放弃是正确的时候才会这样做。举例,pipe的读写只有当p->killed非真的时候才会返回。。最终代码会返回到trap,然后在其中重新检查flag并且退出。

某些xv6的sleep循环没有检查p->killed,这是因为这部分在多步系统调用中的代码是源自的。 virtio driver是一个例子:它不会检查p->killed,因为一个磁盘操作可能是一系列必要的写操作中的一个,这一系列操作保证了文件系统的正确状态。一个在等待磁盘IO时被kill的进程不会退出,直到它完成了当前的系统调用,usertrap发现了其flag为真。

1.9 Real world

Xv6的scheduler实现了一个简单的调度策略,即轮流执行各个进程。这种策略叫做 round robin。真正的OS会实现更加复杂的策略,比方说给进程排优先级。思路是scheduler会更青睐于高优先级的进程。这些策略会变得复杂,因为有很多需要达成的目标:举例保证公平性和吞吐量。而且,复杂的策略可能会导致意想不到的影响比如说优先级反转和convoys。优先级反转会发生在低优先级和高优先级进程分享同一个锁的情况,此时低优先级的进程会使得高优先级的进程陷入等待。一个长的等待convoy会在很多高优先级的进程等待低优先级进程释放分享的锁的情况,一旦一个convoy形成了,这会持续很长时间。为了避免这种情况,调度器需要添加更加复杂的处理机制。

sleepwakeup是非常简单和有效的同步方法,但是还有很多其他方式可以完成同步。我们在本章的开头已经了解到,第一个挑战就是如何避免丢失唤醒。最早的Unix的sleep就是简单的关闭中断,因为Unix只是一个单核系统。因为xv6跑在多核处理器上,所以需要添加 一个隐含锁来实现sleep。FreeBSD的msleep也采用了相同的策略。Plan 9的sleep用了一个回调函数,在沉睡之前持有了一个调度锁;这个函数类似于最后一次检查沉睡条件,来避免丢失唤醒。Linux的sleep使用了一个隐式的进程队列,叫做等待队列,而不是一个channel;这个队列有它自己的锁。

wakeup在整个进程表上面扫描对应的chan是非常低效的。一个更好的处理方案的用一个持有所有沉睡于此的进程的列表的数据结构来替代,比如说Linux 的等待队列。Plan 9 sleepwakeup中被叫做汇合点或者Rendez。很多线程库中称其为状态变量;在这种语义中,sleepwakeup操作被叫做waitsignal。所有这些机制都有着一个相同的特点:沉睡条件被某种在沉睡中原子化释放的锁保护。

wakeup的实现会唤醒所有在特定channel上沉睡的进程,而确实有时会有很多的进程在特定的channel上面等待。OS会调度所有的进程,他们会去竞争检查沉睡条件。这样的进程有时被称为 thundering herd,我们最好能避免这种情况。很多条件变量有两个原语:wakeupsignal,一个唤醒一个进程,一个唤醒所有进程。

Semaphore经常被用来同步。这个计数一般都会与某些东西相关,比如说pipe中可以读的字节数,某个进程拥有的状态为ZOMBIE的子进程。把一个隐式的计数当作这个抽象的一部分避免了丢失唤醒的问题:隐式的计数就是wakeup发生的次数。这个计数也可以避免虚假的唤醒和thundering herd问题。

终止进程和清理进程给xv6引入了很多的复杂性。在很多的OS中,这会更加复杂,举个例子,因为被害者进程可能会在kernel中沉睡,清除掉它的栈需要很仔细的程序。很多OS利用隐式的异常处理机制来清除栈,比如说longjmp。而且,有另外的事件也可以导致沉睡进程被kill,即使该事件目前还没有发生。举个例子,当一个Unix进程在沉睡,另一个进程发送了一个signal给他。在这种情况下,这个进程会从中断系统调用中返回-1,并带有一个错误代码EINTR。这个应用可以检查这些值,并且决定做什么。Xv6不支持signal,所以没那么复杂。

Xv6对kill的支持不是完全的:需要在sleep loops中检查p->killed。一个相关的问题是,即使有一个sleep循环来检查p->killed,sleepkill之间还是会有竞争;后者可能会设置p->killed并且在受害者进程检查完之后,但是在调用sleep之前尝试唤醒它。如果这种情况出现,那么受害者进程将不会发现p->killed直到它等待的条件出现。这可能会有点晚了(当 virtio driver返回一个受害者进程正在等待的磁盘块),或者不会出现(如果受害者进程正在等待控制台输入,但是使用者并不输入)。

一个真实的OS会在一个常数时间找到需要被释放的proc,而不是一个线性时间。因为实现简单,所以xv6使用了线性扫描的方法。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值