计算机上所有可运行的软件,被组织成若干顺序进程,简称进程。
一个进程就是一个正在执行程序的实例,包括程序计数器、寄存器和变量的当前值。
从概念上来讲,每个进程都拥有它自己的虚拟CPU。
实际的CPU在各个进程之间来回切换,这种快速的切换被称作多道程序设计。
一个进程是某种类型的一个活动,它有程序、输入、输出以及状态。单个处理器可以被若干进程共享,它使用某种算法决定何时停止一个进程的工作,并转而为另一个进程提供服务。
值得注意的是,如果一个程序运行了两次,则算作两个进程。
操作系统虽然能够使它们共享代码,因此只有一个副本存储在内存中,但是那只是一个技术性细节,并不会改变有两个进程正在运行的概念。
在系统当中,有四种主要事件会导致进程的创建:
1、系统初始化。
2、正在运行的程序执行了创建进程的系统调用。
3、用户请求创建一个新的进程。
4、一个批处理作业的初始化。
启动操作系统时,通常会创建若干进程。其中某些是前台进程,也就是与用户交互并且替他们完成工作的进程。
其他的是后台进程,这些进程与特定的用户没有关系,并且具有某些特别的功能。
例如,设计一个后台进程来接收发来的电子邮件,这个进程大部分时间都在睡眠,但是当电子邮件到达时就突然被唤醒了。
此类进程被称之为守护进程。
一个正在运行的进程可以发出系统调用,创建一个或者多个进程协助工作。
例如,如果有大量数据要通过网络调取并进行顺序处理,那么创建一个进程获取数据,把获取到的数据放入共享缓冲区,让第二个进程取走数据项并进行处理。
在多处理机中,让每个进程在不同的CPU上运行会使整个作业运行的更快。
在交互式系统当中,键入一个命令或者点击一个图标就可以启动一个程序。
这两个动作中的任何一个都会开始一个新的进程,并在其中运行所选择的程序。
在Microsoft Windows当中,多数情形是,在一个线程开始时,它并没有窗口,但是它可以创建一个或者多个窗口。
在Unix和Windows系统中,用户可以打开多个窗口,每个窗口都会运行一个进程。
最后一种创建进程方式仅在大型机的批处理系统当中应用。用户在这种系统中提交批处理作业。
在操作系统认为有资源可以运行另一个作业时,它就会创建一个新的进程,并运行其输入队列中的下一个作业。
从技术上看,上述所有情形,新进程都是基于一个已经存在的线程执行了一个用于创建线程的系统调用而创建的。
这个进程可以是一个运行的用户进程、一个由键盘或鼠标启动的系统进程或者一个批处理管理进程。
这个进程所做的工作是,执行一个用来创建新进程的系统调用。
这个系统调用,通知操作系统创建一个新的进程,并且直接或者间接地指定该进程中运行的程序。
在Unix系统中,只有一个系统调用可以用来创建新的进程:fork。
这个系统调用会创建一个与调用进程相同的副本。这两个进程拥有相同的内存映像、同样的环境字符串和同样的打开文件。
通常,子程序会接着执行execve或者一个类似的系统调用,用以修改其内存映像并运行一个新的程序。
之所以要安排两步建立进程,是为了在fork之后,execve之前,允许子进程处理其文件描述符。
这样可以完成标准输入文件、标准输出文件和标准错误文件的重定向。
在Windows系统中,一个Win32函数调用CreateProcess既处理进程的创建,也负责把正确的程序装入新的进程。
该调用有10个参数,其中包括要执行的程序、输入给该程序的命令行参数,各种安全属性、有关打开的文件是否继承的控制位、优先级信息、该进程所需要的创建的窗口规格(如果有)以及指向一个结构的指针,在该结构中新创建进程的信息被返回给调用者。
除了CreateProcess,Win32中大约有100个其他的函数用于处理进程的管理、同步以及相关的事务。
在Unix和Windows中,进程创建后,父进程和子进程各自拥有不同的地址空间。如果其中某个进程在其地址空间修改了一个字,这个修改对于其他进程是不可见的。
在Unix当中,子进程的初始地址是父进程的一个副本,但是这里涉及两个不同的地址空间,不可写的内存区是共享的。
某些Unix的实现使程序正文在两者之间共享,因为它不能被修改。
或者,子进程共享父进程的所有内存,但这种情况下内存通过写实复制共享,这意味一旦两者之一想要修改部分内存,则这块内存首先被明确复制,以确保修改事件发生在私有内存区域。
可写的内存是不可以被共享的。然而,有些情况对于一个新创建的进程而言,确实有可能共享创建者的其他资源,诸如打开的文件等。
在Windows当中,从一开始父进程的地址空间和子进程的地址空间就是不同的。
进程在创建后,开始运行,完成其工作。但是永恒是不存在的,进程也一样。
进程的终止,通常由下列条件引起:
1、正常退出(自愿)
2、出错退出(自愿)
3、严重错误(非自愿)
4、被其他进程杀死(非自愿)
多数进程由于完成了它们的工作而终止。在Unix中调用的是exit。在Windows中,相关的调用是ExitProcess。
严重错误,通常是由于程序的错误而导致的非自愿结束。
例如,执行了一条非法指令、引用了不存在的内存、或者除零等。
在某些系统中,进程可以通知操作系统,它希望自行处理某些类型的错误。在这类错误中,进程会收到信号(被中断),而不是在这类错误发生后强制终止。
某个进程执行一个系统调用通知操作系统杀死某个其他进程。
在Unix中,这个系统调用是kill。
在Win32中,对应的函数时TerminateProcess。
这两种情形,“杀手”都必须得到确定的授权才可以进行动作。
在有些系统中,当一个进程终止时,无论是自愿还是其他原因,由该进程所创建的所有进程一律被杀死。不过,Unix和Windows并非是这种工作方式。
某些系统中,当进程创建了另一个进程,父进程和子进程会以某种形式继续保持关联。子进程自身可以创建更多的进程,组建一个进程的层次结构。
在Unix中,进程和它所有的子进程以及后裔会组成一个进程组。当用户从键盘发出一个信号时,该信号被送给当前与键盘相关的进程组中的所有成员(它们通常是在当前窗口创建的所有活动进程)。
每个进程可以分别捕获幸好,忽略信号或者采取默认的动作,即将该信号杀死。
Unix中,一个称为init的特殊进程出现在启动映像中。当它开始运行时,读入一个说明终端数量的文件。紧接着,为每个终端创建一个新进程。这些进程会等待用户的登录。
如果有一个用户登陆成功,该登录进程就执行一个shell准备接收命令,这些接收的命令会启动更多的进程。
以此类推,这样,整个系统中,所有的进程都属于以init为根的一棵树。
而在Windows当中,则并不是这样。所有的进程都是地位相同的。唯一类似于进程层次的暗示是在创建进程时,父进程会得到一个特别的令牌(句柄),该句柄可以用来控制子进程。
但是父进程有权利把这个令牌传送给某个其他进程,这样就不存在进程层次了。
而在Unix当中,进程就不同剥夺子进程的“继承权”。
尽管每个进程都是一个独立的实体,其内部有自己的程序计数器和内部状态,但是,进程之间进场需要互相作用。
当一个进程在逻辑上不能继续运行时,它就会被阻塞,典型的例子是它在等待可以使用的输入。
还有可能是,一个概念上能够运行的进程被迫终止,因为操作系统调度另一个进程占用了CPU。
这两种情况是完全不同的。第一种,进程挂起是自身固有原因造成的。第二种则是由于系统技术上的原因(没有足够的CPU,所以不能让每个进程都有一台私用的处理器)。
由此,我们可以延伸出三种状态:
1、运行态(该时刻进程实际占用CPU)
2、就绪态(可以运行,但是因为其他进程正在运行而暂时停止)
3、阻塞态(除非某种外部时间发生,否则进程不能运行)
前两种状态在逻辑上是类似的。处于这两种状态的进程都是可以运行,只是对于第二种状态暂时没有CPU分配给它。
第三种状态则与前面两种完全不同,处于该状态进程不能运行,即使CPU空闲也不行。
三种状态有四种可能的转换关系。
在操作系统发现进程不能继续运行下去时,发生转换。
在某些系统中,进程可以执行一个诸如pause的系统调用来进入阻塞状态。
在其他系统中,包括Unix,当一个进程从管道或者设备文件读取数据时,如果没有有效的输入存在,则进程表会被自动阻塞。
有两种转换是由进程调度程序引起的,进程调度程序是操作系统的一部分。
系统认为一个进程占用CPU时间过长,决定让其他进程使用CPU时间,从而发生转换。
在系统已经让所有的进程享有了它们应有的公平待遇后重新轮到的第一个进程再次占用CPU,状态从而发生转换。
调度程序的主要工作就是决定应当运行哪个进程,何时运行以及应该运行多长时间。
当个进程等待的一个外部事件发生时,则会发生转换。如果此时没有其他进程运行,则立即进入运行态,否则处于就绪态等待进程调度程序安排CPU。
操作系统最底层是调度程序,在它上面有许多的进程。所有关于中断处理,启动进程和停止进程的具体细节都隐藏在调度程序当中。
实际上,调度程序时非常短小的程序。
操作系统的其他部分都被简单地组织成进程的形式,不过,很少有真实的系统是以这种理想方式构造的。
为了实现进程模型,操作系统维护着一个表格(一个结构数组),即进程表。
每个进程都会占用一个进程表项(进程控制块)。该表项包括了进程状态的重要信息,包括程序计数器、堆栈指针、内存分配状况、所打开文件的状态、账号和调度信息,以及其他在进程由运行态转换到就绪态或者阻塞态必须保存的信息,从而保证该进程随后能够再次启动,就像从未被中断过一样。
在了解进程表后,就可以对单个(或者每一个)CPU上如何维持多个顺序进程的错觉进行更多的阐述。
与每一个I/O类关联的是一个称作中断向量的位置(靠近内存底部的固定区域)。它包含中断服务程序的入口地址。
假设,当一个磁盘发生中断,用户进程正在运行,则中断硬件将程序计数器、程序状态字、有时还有一个或者多个寄存器压入堆栈,计算器随机跳转到中断向量所指示的地址。
这是硬件完成的所有操作,然后打开软件,特别是中断服务例程会接管一切剩余的工作。
所有的中断都是从保存寄存器开始的,对于进程而言,通常是保存在进程表项中。随后,会从堆栈中删除由中断硬件机制存入堆栈的那部分信息,并将堆栈指针指向一个由进程处理程序所使用的临时堆栈,
一些诸如保存寄存器值和设置堆栈指针等操作,无法用C语言进行描述,所以这些操作通过一个短小的汇编语言例程来完成。
通常,该例程可以供所有的中断使用,因为无论中断是怎样引起的,有关保存寄存器的工作是完全一样的。
当该例程结束后,它会调用一个C过程处理某个特定的中断类型个剩下的工作。
在完成有关操作后,大概就会让某些进程就绪,紧接着调用调度程序,决定随后应该运行哪个进程。
随后,将控制转给一段汇编代码,为当前的进程装入寄存器值以及内存映射并启动该进程运行。
1、硬件压入堆栈程序计数器等
2、硬件从中断向量装入新的程序计数器
3、汇编语言过程保存寄存器值
4、汇编语言过程设置新的堆栈
5、C中断服务例程运行(典型地读和缓冲输入)
6、调度程序决定下一个即将运行的进程
7、C过程返回至汇编代码
8、汇编语言过程开始运行新的当前进程
值得注意的是,各种系统之间的细节会有所不同。
一个进程在执行过程中,可能被中断数千次。但关键是每次中断后,被中断的进程都返回到与中断发生前的完全相同的状态。
采用多道程序设计可以提高CPU的利用率。严格来说,如果进程用于计算的平均时间是进程在内存中停留时间的20%,且内存中同时有5个进程,则该CPU将会一直满载运行。
然而,这个模型在现实中过于乐观,因为它假设这5个进程不会同时等待I/O
Cpu的利用率 = 1 - pn n被称为多道程序设计的道数
假设计算机有8G内存,操作系统以及相关表格占用2G,用户程序占用2G。
这些内存空间允许3个用户程序同时驻留在内存中。
如果80%的时间用于等待I\O,则Cpu的利用率大约是1-0.83,即大约49%。
在增加8GB内存后,可以从3道提升到7道,因而Cpu的利用率提高到79%。
换而言之,第二个8GB内存提高了30%的吞吐量。
如果再增加一个8GB内存,就只能将CPU的利用率从79%提升到91%,吞吐量的提高仅为12%。
通过该模型,用户可以确定,第一次增加内存是一个划算的投资,而第二个并不是。
熬过最苦的日子,做最酷的自己