文章目录
伪并行(pseudoparallelism)
在许多多道程序系统中,CPU 会在进程间快速切换,使每个程序运行几十或者几百毫秒。然而,严格意义来说,在某一个瞬间,CPU 只能运行一个进程,然而我们如果把时间定位为 1 秒内的话,它可能运行多个进程。这样就会让我们产生并行的错觉。有时候人们说的 伪并行(pseudoparallelism) 就是这种情况,以此来区分多处理器系统(该系统由两个或多个 CPU 来共享同一个物理内存)
因为 CPU 执行速度很快,进程间的换进换出也非常迅速,因此我们很难对多个并行进程进行跟踪,所以,在经过多年的努力后,操作系统的设计者开发了用于描述并行的一种概念模型(顺序进程),使得并行更加容易理解和分析,对该模型的探讨,也是本篇文章的主题。下面我们就来探讨一下进程模型
1. 进程模型
在进程模型中,所有计算机上运行的软件,通常也包括操作系统,被组织为若干顺序进程(sequential processes),简称为 进程(process) 。
-
一个进程就是一个正在执行的程序的实例,进程也包括程序计数器、寄存器和变量的当前值。
-
从概念上来说,每个进程都有各自的虚拟 CPU,但是实际情况是 CPU 会在各个进程之间进行来回切换。
-
在任何一个给定的瞬间仅有一个进程真正运行。
-
一个 CPU 只能真正一次运行一个进程的时候,即使有 2 个核(或 CPU),每一个核也只能一次运行一个线程。
-
进程是某一类特定活动的总和,它有程序、输入输出以及状态。单个处理器可以被若干进程共享,它使用某种调度算法决定何时停止一个进程的工作,并转而为另外一个进程提供服务。另外需要注意的是,如果一个进程运行了两遍,则被认为是两个进程。
1.1 进程的创建
操作系统需要一些方式来创建进程。下面是一些创建进程的方式
系统初始化(init)
正在运行的程序执行了创建进程的系统调用(比如 fork)
用户请求创建一个新进程
初始化一个批处理工作
1.1.1 系统初始化
启动操作系统时,通常会创建若干个进程。
-
其中有些是前台进程(numerous processes),也就是同用户进行交互并替他们完成工作的进程。
-
一些运行在后台,并不与特定的用户进行交互,例如,设计一个进程来接收发来的电子邮件,这个进程大部分的时间都在休眠,但是只要邮件到来后这个进程就会被唤醒。还可以设计一个进程来接收对该计算机上网页的传入请求,在请求到达的进程唤醒来处理网页的传入请求。
进程运行在后台用来处理一些活动像是 e-mail,web 网页,新闻,打印等等被称为 守护进程(daemons)。大型系统会有很多守护进程。在 UNIX 中,ps 程序可以列出正在运行的进程, 在 Windows 中,可以使用任务管理器。
1.1.2 系统调用创建
除了在启动阶段创建进程之外,一些新的进程也可以在后面创建。
- 通常,一个正在运行的进程会发出系统调用用来创建一个或多个新进程来帮助其完成工作。
例如,如果有大量的数据需要经过网络调取并进行顺序处理,那么创建一个进程读数据,并把数据放到共享缓冲区中,而让第二个进程取走并正确处理会比较容易些。在多处理器中,让每个进程运行在不同的 CPU 上也可以使工作做的更快。
1.1.3 用户请求创建
- 在许多交互式系统中,输入一个命令或者双击图标就可以启动程序,以上任意一种操作都可以选择开启一个新的进程.
在基本的 UNIX 系统中运行 X,新进程将接管启动它的窗口。在 Windows 中启动进程时,它一般没有窗口,但是它可以创建一个或多个窗口。每个窗口都可以运行进程。通过鼠标或者命令就可以切换窗口并与进程进行交互。
1.1 4 批处理创建
最后一种创建进程的情形会在大型机的批处理系统中应用。用户在这种系统中提交批处理作业。当操作系统决定它有资源来运行另一个任务时,它将创建一个新进程并从其中的输入队列中运行下一个作业。
从技术上讲,在所有这些情况下,让现有流程执行流程是通过创建系统调用来创建新流程的。该进程可能是正在运行的用户进程,是从键盘或鼠标调用的系统进程或批处理程序。这些就是系统调用创建新进程的过程。
该系统调用告诉操作系统创建一个新进程,并直接或间接指示在其中运行哪个程序。
-
在 UNIX 中,仅有一个系统调用来创建一个新的进程,这个系统调用就是
fork
。- 这个调用会创建一个与调用进程相关的副本。
- 在 fork 后,一个父进程和子进程会有相同的内存映像,相同的环境字符串和相同的打开文件。
- 通常,子进程会执行 execve 或者一个简单的系统调用来改变内存映像并运行一个新的程序。
- 例如,当一个用户在 shell 中输出 sort 命令时,shell 会 fork 一个子进程然后子进程去执行 sort 命令。这两步过程的原因是允许子进程在 fork 之后但在 execve 之前操作其文件描述符,以完成标准输入,标准输出和标准错误的重定向。
-
在 Windows 中,情况正相反。
- 一个简单的 Win32 功能调用 CreateProcess,会处理流程创建并将正确的程序加载到新的进程中。这个调用会有 10 个参数,包括了需要执行的程序、输入给程序的命令行参数、各种安全属性、有关打开的文件是否继承控制位、优先级信息、进程所需要创建的窗口规格以及指向一个结构的指针,在该结构中新创建进程的信息被返回给调用者。
- 除了 CreateProcess Win 32 中大概有 100 个其他的函数用于处理进程的管理,同步以及相关的事务。
下面是 UNIX 操作系统和 Windows 操作系统系统调用的对比
在 UNIX 和 Windows 中,进程创建之后,父进程和子进程有各自不同的地址空间。
如果其中某个进程在其地址空间中修改了一个词,这个修改将对另一个进程不可见。
-
在 UNIX 中,子进程的地址空间是父进程的一个拷贝,但是却是两个不同的地址空间;
- 不可写的内存区域是共享的。某些 UNIX 实现是正是在两者之间共享,因为它不能被修改。
- 或者,子进程共享父进程的所有内存,但是这种情况下内存通过 写时复制(copy-on-write) 共享,这意味着一旦两者之一想要修改部分内存,则这块内存首先被明确的复制,以确保修改发生在私有内存区域。
- 再次强调,可写的内存是不能被共享的。但是,对于一个新创建的进程来说,确实有可能共享创建者的资源,比如可以共享打开的文件。
-
在 Windows 中,从一开始父进程的地址空间和子进程的地址空间就是不同的。
1.2 进程的终止
进程在创建之后,它就开始运行并做完成任务。进程早晚会发生终止,但是通常是由于以下情况触发的
正常退出(自愿的)
错误退出(自愿的)
严重错误(非自愿的)
被其他进程杀死(非自愿的)
1.2.1 正常退出(自愿的)
多数进程是由于完成了工作而终止。当编译器完成了所给定程序的编译之后,编译器会执行一个系统调用告诉操作系统它完成了工作。
这个调用在 UNIX 中是 exit
,在 Windows 中是 ExitProcess
。
面向屏幕中的软件也支持自愿终止操作。字处理软件、Internet 浏览器和类似的程序中总有一个供用户点击的图标或菜单项,用来通知进程删除它所打开的任何临时文件,然后终止。
1.2.2 错误退出(自愿的)
进程发生终止的第二个原因是发现严重错误,例如,如果用户执行如下命令
cc foo.c
为了能够编译 foo.c 但是该文件不存在,于是编译器就会发出声明并退出。在给出了错误参数时,面向屏幕的交互式进程通常并不会直接退出,因为这从用户的角度来说并不合理,用户需要知道发生了什么并想要进行重试,所以这时候应用程序通常会弹出一个对话框告知用户发生了系统错误,是需要重试还是退出。
1.2.3 严重错误(非自愿的)
进程终止的第三个原因是由进程引起的错误,通常是由于程序中的错误所导致的。
例如,执行了一条非法指令,引用不存在的内存,或者除数是 0 等。在有些系统比如 UNIX 中,进程可以通知操作系统,它希望自行处理某种类型的错误,在这类错误中,进程会收到信号(中断),而不是在这类错误出现时直接终止进程。
1.2.4 被其他进程杀死(非自愿的)
第四个终止进程的原因是,某个进程执行系统调用告诉操作系统杀死某个进程。
在 UNIX 中,这个系统调用是 kill
。在 Win32 中对应的函数是 TerminateProcess
(注意不是系统调用)。
1.3 进程的层次结构
在一些系统中,当一个进程创建了其他进程后,父进程和子进程就会以某种方式进行关联。子进程它自己就会创建更多进程,从而形成一个进程层次结构。
1.3.1 UNIX 进程体系
在 UNIX 中,进程和它的所有子进程以及子进程的子进程共同组成一个进程组。
当用户从键盘中发出一个信号后,该信号被发送给当前与键盘相关的进程组中的所有成员(它们通常是在当前窗口创建的所有活动进程)。每个进程可以分别捕获该信号、忽略该信号或采取默认的动作,即被信号 kill 掉。
这里有另一个例子,可以用来说明层次的作用,考虑 UNIX 在启动时如何初始化自己。
- 一个称为 init 的特殊进程出现在启动映像中 。
- 当 init 进程开始运行时,它会读取一个文件,文件会告诉它有多少个终端。
- 然后为每个终端创建一个新进程。这些进程等待用户登录。
- 如果登录成功,该登录进程就执行一个 shell 来等待接收用户输入指令,这些命令可能会启动更多的进程,以此类推。
- 因此,整个操作系统中所有的进程都隶属于一个单个以 init 为根的进程树。
1.3.2 Windows 进程体系
相反,Windows 中没有进程层次的概念,Windows 中所有进程都是平等的,
唯一类似于层次结构的是在创建进程的时候,父进程得到一个特别的令牌(称为句柄),该句柄可以用来控制子进程。
然而,这个令牌可能也会移交给别的操作系统,这样就不存在层次结构了。而在 UNIX 中,进程不能剥夺其子进程的 进程权。(这样看来,还是 Windows 比较渣)。
1.4 进程状态
尽管每个进程是一个独立的实体,有其自己的程序计数器和内部状态,但是,进程之间仍然需要相互帮助。
例如,一个进程的结果可以作为另一个进程的输入,在 shell 命令中
cat chapter1 chapter2 chapter3 | grep tree
第一个进程是 cat,将三个文件级联并输出。
第二个进程是 grep,它从输入中选择具有包含关键字 tree 的内容,根据这两个进程的相对速度(这取决于两个程序的相对复杂度和各自所分配到的 CPU 时间片),可能会发生下面这种情况,grep 准备就绪开始运行,但是输入进程还没有完成,于是必须阻塞 grep 进程,直到输入完毕。
当一个进程开始运行时,它可能会经历下面这几种状态