Skip to content

进程

进程概念

早期的计算机一次只能执行一个程序。这种程序完全控制系统,并且访问所有系统资源。 相比之下,现代操作系统允许加载多个程序到内存,以便并发执行。这些改进要求: 对各种程序提供更严格的控制和更好的划分。这些需求导致了进程概念的产生,即进程为执行程序。进程是现代分时操作系统的工作单元

进程

如前所述,进程是执行的程序,这是一种非正式的说法。进程不只是程序代码,程序代码有时称为文本段(或代码段)。 进程还包括当前活动,如程序计数器的值和处理器寄存器的内容等。另外,进程通常还包括:进程堆栈(包括临时数据,如函数参数、返回地址和局部变量)和数据段(包括全局变量)。 进程还可能包括,这是在进程运行时动态分配的内存。

我们强调:程序本身不是进程。程序只是被动实体,如存储在磁盘上包含一些列指令的文件(经常称为可执行文件)。相反,进程是 活动实体,具有一个程序计数器用于表示下个执行命令和一组相关资源。当一个可执行文件被加载到内存时,这个程序就成为进程。 加载可执行文件通常有两种方法: 双击一个代表可执行文件的图标或在命令行上输入可执行文件的名称。

虽然两个进程可以与同一程序相关联,但是当作两个单独的执行序列。例如,多个用户可以运行电子邮件的不同副本,或者 同一用户可以调用 Web 浏览器程序的多个副本。每个都是单独进程;虽然文本段相同,但是数据、堆及堆栈段却不同。

进程状态

进程在执行时会改变状态。进程状态,部分取决于进程的当前活动。每个进程可能处于以下状态;

  • 新的:进程正在创建
  • 运行:指令正在执行
  • 等待:进程等待发生某个事件(如 I/O 完成或收到信号)
  • 就绪:进程等待分配处理器
  • 终止:进程已经完成执行
进程控制块

操作系统内的每个进程表示,采用进程控制块(Porcess Control Block, PCB),也称为任务控制块。 它包含许多与某个特定进程相关的信息:

  • 进程状态:状态可以包括新的、就绪、运行、等待、停止等
  • 程序计数器:计数器表示进程将要执行的下个指令的地址
  • CPU 寄存器:根据计算机体系结构的不同,寄存器的类型和数量也会不同。它们包括累加器、索引寄存器、堆栈指针、通用寄存器和其他 条件码信息寄存器。在发生中断时,这些状态信息与程序计数器一起需要保存,以便进程以后能正确地继续执行
  • CPU 调度信息:这类信息包括进程优先级、调度队列的指针和其他调度参数
  • 内存管理信息:根据操作系统使用的内存系统,这类信息可以包括基地址和界限寄存器的值、页表或段表
  • 记账信息:这类信息包括 CPU 时间、实际使用时间、时间期限、记账数据、作业或进程数量等
  • I/O 状态信息:这类信息包括分配给进程的 I/O 设备列表、打开文件列表等

简而言之,PCB 简单作为这些信息的仓库,这些信息随着进程的不同而不同

线程

如果一个进程是一个只能进行单个执行线程的程序。例如,如果一个进程运行一个字处理器程序,那么只能执行单个指令线程。 这种单一控制线程使得进程一次只能执行一个任务。例如,用户不能在同一进程内,同时输入字符和拼写检查。许多现代操作系统扩展了进程概念, 以便支持一次能够执行多个线程。这种特征对多核系统尤其有益,因为可以并行运行多个线程。 在支持线程的系统中,PCB 被扩展到包括每个线程的信息。

进程调度

多道程序设计的目标是,无论何时都有进程运行,从而最大化 CPU 利用率。分时系统的目的是在进程之间快速切换 CPU,以便用户在程序运行时能与其交互。 为了满足目标,进程调度器选择一个可用进程(可能从多个可用进程集合中)到 CPU 上执行。

调度程序

进程在整个生命周期中,会在各种调度队列之间迁移。操作系统为了调度必须按一定方式从这些队列中选择进程。进程选择通过适当调度器调度程序来执行

通常,对于批处理系统,提交的进程多于可以立即执行的。这些进程会被保存到大容量存储设备(通常为磁盘)的缓冲池,以便以后执行。长期调度程序或作业调度程序 从该池中选择进程,加到内存,以便执行。短期调度程序CPU调度程序从准备执行的进程中选择进程,并分配 CPU

这两种调度程序的主要区别是执行频率。短期调度程序必须经常为 CPU 选择新的进程。进程可能执行几毫秒(ms),就会等待 I/O 请求。通常,短期调度程序每 100ms 至少执行一次。由于执行之间的时间短,短期调度程序必须快速。如果花费 10ms 来确定执行一个运行 100ms 的进程,那么 10/(100 + 10) = 9% 的 CPU 时间会用在(浪费在)调度工作上

长期调度程序执行并不频繁;在新进程的创建之间,可能有几分钟间隔。长期调度程序控制多道程序程度(内存中的进程数量)。如果多道程序程度稳定,那么创建进程的平均速度必须等于进程离开系统的平均速度。因此,只有在进程离开系统时,才需要长期调度程序的调度。由于每次执行之间的更长时间间隔,长期调度程序可以负担得起更多时间,以便决定应该选择执行哪个进程

重要的是,长期调度程序进程认真选择。通常,大多数进程可分为:I/O 为主或 CPU 为主。I/O 密集型进程,执行 I/O 比执行计算需要花费更多时间。相反,CPU 密集型进程很少产生 I/O 请求,而是将更多时间用于执行计算。重要的是,长期调度程序应该选择 I/O 密集型和 CPU 密集型的合理进程组合。如果所有进程都是 I/O 密集型的,那么就绪队列几乎总是为空,从而短期调度程序没有什么可做。如果所有进程都是 CPU 密集型的,那么 I/O 等待队列几乎总是为空,从而设备没有得到使用,因而系统会不平衡。为了使得性能最佳,系统需要 I/O 密集型和 CPU 密集型的进程组合

有的系统,可能没有或极少采用长期调度程序。例如,UNIX 或微软 Windows 的分时系统通常没有长期调度程序,只是简单将所有新进程放于内存,以供短期调度程序使用。这些系统的稳定性取决于物理限制(如可用的终端数)或用户的自我调整。如果多用户系统性能下降到令人难以接受,那么有的用户就会退出

有的操作系统如分时系统,可能引入一个额外的中期调度程序,中期调度程序的核心思想是可将进程从内存(或从 CPU 竞争)中移出,从而降低多道程序程度。之后,进程可被重新调入内存,并从中断处继续执行。这种方案称为交换。通过中期调度程序,进程可换出,并在后来可换入。为了改善进程组合,或者由于内存需求改变导致过度使用内存从而需要释放内存,就有必要使用交换

上下文切换

中断导致 CPU 从执行当前任务改变到执行内核程序。这种操作在通用系统中经常发生。当中断发生时,系统需要保存当前运行在 CPU 上的进程的上下文,以便在处理后能够恢复上下文,即先挂起进程,再恢复进程。进程上下文采用进程 PCB 表示,包括 CPU 寄存器的值、进程状态和内存管理信息等。通常,通过执行状态保存,保存 CPU 当前状态(包括内核模式和用户模式);之后,状态恢复重新开始运行。

切换 CPU 到另一个进程需要保存当前进程状态和恢复另一个进程的状态,这个任务称为上下文切换。当进行上下文切换时,内核会将旧进程状态保存在其 PCB 中,然后加载经调度而要执行的新进程的上下文。上下文切换的时间是纯粹的开销,因为在切换时系统并没有做任何有用的工作。上下文切换的速度因机器的不同而有所不同,它依赖于内存速度、必须复制的寄存器数量、是否有特殊指令(如加载或存储所有寄存器的单个指令)。典型速度为几毫秒

进程运行

进程创建

进程在执行过程中可能创建多个新的进程。创建进程称为父进程,而新的进程称为子进程。每个新进程可以再创建其他进程,从而形成进程树

大多数的操作系统对进程的识别采用的是唯一的进程标识符(process identifier, pid),这通常是一个整数值。系统内的每个进程都有一个唯一 pid,它可以用作索引,以便访问内核中的进程的各种属性

对于 UNIX 和 Linux 系统,我们可以通过 ps 命令得到一个进程列表。例如,命令

1
ps -el

可以列出系统中的所有当前活动进程的完整信息

一般来说,当一个进程创建子进程时,该子进程会需要一定的资源(CPU 时间、内存、文件、I/O 设备等)来完成任务。子进程可以从操作系统那里直接获得资源,也可以只从父进程那里获得资源子集。父进程可能要在子进程之间分配资源或共享资源(如内存或文件)。限制子进程只能使用父进程的资源,可以防止创建过多进程,导致系统超载

在 UNIX 操作系统中,每个进程都用一个唯一的整型进程标识符来标识。通过系统调用 fork(),可创建新进程。新进程的地址空间复制了原来进程的地址空间。这种机制允许父进程与子进程轻松通信。这两个进程(父与子)都继续执行处于系统调用 fork() 之后的指令,但有一点不同:对于新(子)进程,系统调用 fork() 的返回值为 0;而对于父进程,返回值为子进程的进程标识符(非零)。

通常,在系统调用 fork() 之后,有个进程使用系统调用 exec(),以用新程序来取代进程的内存空间。系统调用 exec() 加载二进制文件到内存中(破坏了包含系统调用 exec() 的原来程序的内存内容),并开始执行。采用这种方式,这两个进程能相互通信,并能按各自方法运行。父进程能够创建更多子进程,或者如果在子进程运行时没有什么可做,那么它采用系统调用 wait() 把自己移出就绪队列,直到子进程终止。因为调用 exec() 用新程序覆盖了进程的地址空间,所以调用 exec() 除非出现错误,不会返回控制。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>

int main() {
    pid_t pid;

    pid = fork();
    printf("Return Value = %d\n", pid);

    if (pid < 0) {
        fprintf(stderr, "Fork Failed");
        return 1;
    } else if (pid == 0) {
        execlp("/bin/ls", "ls", NULL);
    } else {
        printf("Wait\n");
        wait(NULL);
        printf("Child Complete\n");
    }
    return 0;
// 运行结果:
// Return Value = 61613
// Wait
// Return Value = 0
// a.out                   test.c
// Child Complete
}

C 程序说明了上述 UNIX 系统调用。这里有两个不同进程,但运行同一程序。这两个进程的唯一差别是:子进程的 pid 值为 0,而父进程的 pid 值大于 0(实际上,它就是子进程的 pid)。子进程继承了父进程的权限、调度属性以及某些资源,诸如打开文件。通过系统调用 execlp()(这是系统调用 exec() 的一个版本),子进程采用 UNIX 命令 /bin/ls(用来列出目录清单)来覆盖其地址空间。通过系统调用 wait(),父进程等待子进程的完成。当子进程完成后(通过显式或隐式调用 exit()),父进程会从 wait() 调用处开始继续,并且结束时会调用系统调用 exit()。

进程终止

当进程完成执行最后语句并且通过系统调用 exie() 请求操作系统删除自身时,进程终止。这时,进程可以返回状态值(通常为整数)到父进程(通过系统调用 wait())。所有进程资源,如物理和虚拟内存、打开文件和 I/O 缓冲区等,会由操作系统释放。

父进程终止子进程的原因由很多,如:

  • 子进程使用了超过它所分配的资源
  • 分配给子进程的任务,不再需要
  • 父进程正在退出,,而且操作系统不允许无父进程的子进程继续执行

有些系统不允许子进程在父进程已终止的情况下存在。对于这类系统,如果一个进程终止,那么它的所有子进程也应终止。这种现象,称为级联终止,通常由操作系统来启动

当一个进程终止时,操作系统会释放其资源。不过,它位于进程表中的条目还是在的,直到它的父进程调用 wait();这是因为进程表包含了进程的退出状态。当进程已经终止,但是其父进程尚未调用 wait(),这样的进程称为僵尸进程。所有进程终止时都会过渡到这种状态,但是一般而言僵尸只是短暂存在。一旦父进程调用了 wait(),僵尸进程的进程标识符和它在进程表中的条目就会释放

如果父进程没有调用 wait() 就终止,以至于子进程成为孤儿进程,那么这会发生什么?Linux 和 UNIX 对这种情况的处理是:将 init 进程作为孤儿进程的父进程(init 进程是 UNIX 和 Linux 系统内进城树的根进程)进程 init 定期调用 wait(),以便收集任何孤儿进程的退出状态,并释放孤儿进程标识符和进程表条目

进程间通信

操作系统内的并发执行进程可以是独立的或也可以是协作的。如果一个进程不能影响其他进程或受其他进程影响,那么该进程是独立的。显然,不与任何其他进程共享数据的进程是独立的。如果一个进程能影响其他进程或受其他进程所影响,那么该进程是协作的。显然,与其他进程共享数据的进程为协作进程

协作进程需要有一种进程间通信(InterProcess Communication, IPC)机制,以允许进程相互交换数据与信息。

进程间通信有两种基本模型:内存共享消息传递。共享内存模型会建立起一块供协作进程共享的内存区域,进程通过向此共享区域读出或写入数据来交换信息。信息传递模型通过在协作进程间交换信息来实现通信

消息传递对于交换较少数量的数据很有用,因为无需避免冲突。对于分布式系统,消息传递也比共享内存更易实现。共享内存可以快于消息传递,这是因为消息传递的实现经常采用系统调用,因此需要消耗更多时间以便内核介入。与此相反,共享内存系统仅在建立共享内存区域时需要系统调用;一旦建立共享内存,所有访问都可作为常规内存访问,无需借助内核

Chrome: 多进程架构浏览器
许多网站包含活动内容,如 JavaScript、Flash 和 HTML5 等,以便提供丰富的、动态的 Web 浏览体验。遗憾的是,这些 Web 应用程序也可能包含软件缺陷,从而导致响应迟滞,有的甚至导致网络浏览器崩溃。如果一个 Web 浏览器只对一个网站进行浏览,那么这不是一个大问题。但是,现代 Web 浏览器提供标签式浏览,它允许 Web 浏览器的一个实例,同时打开多个网站,而每个标签代表一个网站。要在不同网站之间切换,用户只需点击相应标签。

这种方法的一个问题是:如果任何标签的 Web 应用程序崩溃,那么整个进程(包括所有其他标签所显示的网站)也会崩溃

Google 的 Chrome Web 浏览器通过多进程架构的设计解决这一问题。Chrome 具有三个不同类型的进程:浏览器、渲染器和插件

  • 浏览器进程负责管理用户界面以及磁盘和网络的 I/O。当 Chrome 启动时,创建一个新的浏览器进程。只创建了一个浏览器进程
  • 渲染器进程包含渲染网页的逻辑。因此,它们包含逻辑,以便处理 HTML、JavaScript、图像等等。一般情况下,对应于新标签的每个网站都会创建一个新的渲染进程。因此,可能会有多个渲染进程同时活跃
  • 对于正在使用的每种类型的插件,都有一个插件进程。插件进程不但包含插件本身代码,而且包含额外代码,以便与有关渲染进程和浏览器进程进行通信

多进程方法的优点是:网站彼此独立运行。如果有一个网站崩溃,只有它的渲染进程受到影响;所有其他进程仍然安然无恙。此外,渲染进程在沙箱中运行,这意味着访问磁盘和网络 I/O 是受限制的,进而最大限度地减少任何安全漏洞的影响

对具有多个处理系统的最新研究标明:在这类系统上,消息传递的性能要优于共享内存。共享内存会有告诉缓存一致性问题,这是由共享数据在多个高速缓存之间迁移而引起的。随着系统的处理核的数量的日益增加,可能导致消息队列作为 IPC 的首选机制