现在的时分(time-sharing)多任务(multi-task)操作系统架构通常都是用所谓的“时间分片(time quantum or time slice)”方式进行抢占式(preemptive)轮转调度(round-robin式)。
更复杂的可能还会加入优先级(priority)的机制。
这个时间分片通常是很小的,一个线程一次最多只能在 cpu 上运行比如10-20ms 的时间(此时处于running 状态),也即大概只有0.01秒这一量级,时间片用后就要被切换下来放入调度队列的末尾等待再次调度。(也即回到ready状态)
注:如果期间进行了 I/O 的操作还会导致提前释放时间分片,并进入等待队列。又或者是时间分片没有用完就被抢占,这时也是回到ready状态。
这一切换的过程称为线程的上下文切换(context switch),当然cpu不是简单地把线程踢开就完了,还需要把被相应的执行状态保存到内存中以便后续的恢复执行。
Java的线程状态是服务于监控的
传统的I/O都是阻塞式(blocked)的,原因是I/O操作比起cpu来实在是太慢了,可能差到好几个数量级都说不定。如果让cpu去等I/O操作,很可能时间片都用完了,I/O 操作还没完成呢,不管怎样,它会导致cpu的利用率极低。
所以,解决办法就是:一旦线程中执行到 I/O 有关的代码,相应线程立马被切走,然后调度ready队列中另一个线程来运行。
这时执行了I/O的线程就不再运行,即所谓的被阻塞了。它也不会被放到调度队列中去,因为很可能再次调度到它时,I/O 可能仍没有完成。
线程会被放到所谓的等待队列中,处于上图中的 waiting 状态:
当然了,我们所谓阻塞只是指这段时间cpu暂时不会理它了,但另一个部件比如硬盘则在努力地为它服务。cpu与硬盘间是并发的。如果把线程视作为一个job,这一job由cpu与硬盘交替协作完成,当在cpu上是waiting时,在硬盘上却处于running,只是我们在操作系统层面讨论线程状态时通常是围绕着 cpu 这一中心去述说的。
而当I/O完成时,则用一种叫中断(interrupt)的机制来通知 cpu:
也即所谓的“中断驱动(interrupt-driven)”,现代操作系统基本都采用这一机制。
某种意义上,这也是控制反转(IoC)机制的一种体现,cpu不用反复去询问硬盘,这也是所谓的“好莱坞原则”—Don’t call us, we will call you.好莱坞的经纪人经常对演员们说:“别打电话给我,(有戏时)我们会打电话给你。”
在这里,硬盘与cpu的互动机制也是类似,硬盘对 cpu 说:”别老来问我I\O做完了没有,完了我自然会通知你的“
当然了,cpu 还是要不断地检查中断,就好比演员们也要时刻注意接听电话,不过这总好过不断主动去询问,毕竟绝大多数的询问都将是徒劳的。
cpu 会收到一个比如说来自硬盘的中断信号,并进入中断处理例程,手头正在执行的线程因此被打断,回到ready队列。而先前因I/O而waiting的线程随着I/O的完成也再次回到ready队列,这时cpu可能会选择它来执行。
另一方面,所谓的时间分片轮转本质上也是由一个定时器定时中断来驱动的,可以使线程从running回到ready状态: