现代操作系统使用分时技术管理多个运行程序,对用户来说似乎是同步执行的。当然,如果机器上有多个CPU,会有多个程序真正同步运行。但是为了简单起见,我们假设只有一个处理器,在这种情况下同步只是一种表面现象。
操作系统将运行程序的每个实例表述为进程(Unix术语)或任务(Windows术语)。因此,同时执行的单个程序的多个调用(例如vi文本编辑器的同步会话)是各自独立的进程。在只有一个CPU的机器上,进程必须“轮换”操作。为了形象地说明,让我们假定每个“周期”(称为时间片)的长度为30毫秒。
当一个进程运行了30毫秒以后,硬件计时器发出一个中断,导致操作系统运行。我们假定这个进程被别的进程抢占了时间片。操作系统保存被中断进程的当前状态,因此以后可以恢复其状态,然后选择可以得到时间片的下一个进程。这个过程称为环境切换,因为CPU的执行环境从一个进程切换到另一个进程。这个循环无限重复。
轮换状态可能提前终结。例如,当进程需要执行输入/输出时,最终会调用操作系统中的一个执行低层硬件操作的函数,比如说调用C库函数scanf()会导致进行Unix OS read()系统调用,他需要与键盘驱动程序进行交互。这样,进程将它的轮换安排让给操作系统,提前结束了轮换状态。
这种情况说明了一个问题,给定进程时间片的调度是相当随机的。用户思考然后按下一个键所花的时间也是随机的,因此无法预测下次时间片何时开始。而且,如果调试的是多线程程序,则不知道将要调度的线程次序,这会使调试更加困难。
更细致地看:操作系统中有一个进程表,列出了关于当前所有进程的信息。一般来说,每个进程在进程表中被标记为Run状态或Sleep状态。让我们来看一个示例,其中一个运行程序到达需要从键盘读取输入的位置。正如刚刚提到的,这种情况会结束进程的轮换状态。因为进程现在在等待I/O完成,所以操作系统将其标记为处于Sleep状态,使它无资格占用时间片。因此,在Sleep状态意味着进程被阻塞,等待某个事件的发生。当这个事件最终发生后,操作系统会将它在进程表中的状态改回到Run。
非I/O事件也可以触发进程变成Sleep状态。例如,如果一个父进程创建了一个子进程,并调用wait(),那么父进程会阻塞,直到子进程完成它的工作并且结束。同样,这件事发成的确切时间是无法预测的。
而且,在Run状态中并不表示进程实际在CPU上执行;相反,这仅意味着它准备运行,即有资格得到CPU的时间片。一旦发生了环境切换,操作系统就根据进程表从当前处于Run状态的进程中选择一个进程占用CPU的一个周期。操作系统使用这个调度过程来选择新的环境,保证任何给定进程都能获得时间片,因而最终会完成,但是不承诺它会收到哪些时间片。因此,等待的事件发生后,Sleep进程真正“醒来“的确切时间是随机的,进程完成的准确速率也是随机的。
线程与进程非常类似,只是线程占用的内存比进程少,创建线程和在两个线程之间切换所需的时间较少。事实上,线程有时被称为“轻量级“进程,根据线程系统和运行时环境,它们甚至可能被作为操作系统的进程实现。像使用进程完成工作的程序一样,多线程应用程序一般会执行一个main()进程,该过程创建一个或多个子线程。父线程main()也是线程。
进程和线程之间的主要区别是:与进程一样,虽然每个线程有自己的局部变量,但是多线程环境中父程序的全局变量被所有线程共享,并作为在线程之间通信的主要方法。(虽然也可以在Unix进程之间共享全局变量,但是这样做不方便)。
在Linux系统上,可以通过运行命令ps axH来查看系统上当前的所有进程和线程。
虽然有非抢占线程系统,但是pthreads使用的是抢占线程管理策略,程序中的一个线程可能在任何时候被另一个线程中断。因此,上面描述的进程在时间共享系统中的随机性要素,同样出现在多线程程序的行为中。所以,使用pthreads开发的应用程序中有些程序错误不太容易重视。
Reference:
摘录自《软件调试的艺术》(The Art of Debugging with GDB, DDD and Eclipse)