面试基础篇|操作系统|进程

  • A+
所属分类:linux技术
摘要

关于进程和线程方面的知识相信大家都很熟悉,因为这是操作系统里面最基础也是最重要的内容。而且在面试的时候,面试官很喜欢问这方面的问题,所以有必要深刻的理解一下这部分的内容。


进程

关于进程和线程方面的知识相信大家都很熟悉,因为这是操作系统里面最基础也是最重要的内容。而且在面试的时候,面试官很喜欢问这方面的问题,所以有必要深刻的理解一下这部分的内容。

首先,什么进程?

进程的概念

简言之,一个进程就是一个正在执行程序的实例,包括程序计数器、寄存器和变量的当前值。从概念上说,每个进程拥有它自己的虚拟CPU。当然,实际上真正的CPU在各进程之间来回切换。

这里要把进程和程序区分开,程序是进程的一部分,进程主要包括以下内容:

  • 文本段(text section)(或代码段(code section))
  • 程序计数器(programcounter)的值
  • 处理器寄存器的内容
  • 进程堆栈(stack)(包括临时数据,如函数参数、返回地址和局部变量)
  • 数据段(data section)(包括全局变量)
  • 堆(heap)(进程运行时动态分配的内存)

可以看到,进程与程序的区别在于:

  1. 进程是动态的,程序是静态的

    • 程序是有序代码的集合
    • 进程是程序的执行,进程有核心态/用户态
  2. 进程是暂时的,程序的永久的

    • 进程是一个状态变化的过程
    • 程序可长久保存
  3. 进程与程序的组成不同

    进程的组成包括程序、数据和进程控制块

一个进程是某种类型的一个活动,它有程序、输入、输出以及状态。单个处理器可以被若干进程共享,它使用某种调度算法决定何时停止一个进程的工作,并转而为另一个进程提供服务。

程序本身不是进程。程序只是被动(passive) 实体,如存储在磁盘上包含一系列指令的文件(经常称为可执行文件(executable file) )。相反,进程是活动(active) 实体,具有一个程序计数器用于表示下个执行命令和一组相关资源。当一个可执行文件被加载到内存时,这个程序就成为进程。

简言之,程序只是进程的一部分。总结一下进程的特征:

  • 动态性:进程是程序的一次执行过程,是临时的,有生命期的,是动态产生,动态消亡的
  • 并发性任何进程都可以同其他进行一起并发执行;
  • 独立性:进程是系统进行资源分配和调度的一个独立单位
  • 结构性:进程由程序,数据和进程控制块三部分组成。

下面来看进程是如何实现的。

进程实现

为了实现进程模型,操作系统维护着一张表格(一个结构数组),即进程表(process table)每个进程占用一个进程表项,称为进程控制块(Process Control Block, PCB)。表项包含了进程状态的重要信息,包括程序计数器、堆栈指针、内存分配状况、所打开文件的状态、账号和调度信息,以及其他在进程由运行态转换到就绪态或阻塞态时必须保存的信息,从而保证该进程随后能再次启动,就像从未被中断过一样。

进程控制块的关键字段:

面试基础篇|操作系统|进程

第一列中的字段与进程管理有关。其他两列分别与存储管理和文件管理有关。

接下来看一下进程是如何创建的。

进程创建

关于进程创建的时机,主要有4种主要事件导致进程的创建:

1、系统初始化

启动操作系统时,系统会创建若干进程。有和用户交互的前台进程,同时也有很多后台进程。停留在后台处理诸如电子邮件、Web页面、新闻、打印之类活动的进程称为守护进程(daemon)

2、执行了正在运行的进程所调用的进程创建系统调用。

一个正在运行的进程经常发出系统调用,以便创建一个或多个新进程协助其工作。

3、用户请求创建一个新进程。

在交互式系统中,键入一个命令或者点(双)击一个图标就可以启动一个程序。

4、一个批处理作业的初始化。

用户在批处理系统中提交批处理作业。在操作系统认为有资源可运行另一个作业时,它创建一个新的进程,并运行其输入队列中的下一个作业。

可以看到,在所有这些情形中,新进程都是由于一个已存在的进程执行了一个用于创建进程的系统调用而创建的。创建进程称为父进程,而新的进程称为子进程。每个新进程可以再创建其他进程,从而形成进程树( process tree)。

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

限制子进程只能使用父进程的资源,可以防止创建过多进程,导致系统超载。

当进程创建新进程时,可有两种执行可能:

  • 父进程与子进程并发执行。
  • 父进程等待,直到某个或全部子进程执行完。

新进程的地址空间也有两种可能:

  • 子进程是父进程的复制品(它具有与父进程同样的程序和数据)。
  • 子进程加载另一个新程序。

我们具体来看一下Unix和Windows系统下进程的创建。

Unix系统的进程创建

在UNIX系统中,只有一个系统调用可以用来创建新进程:fork。这个系统调用会创建一个与调用进程相同的副本。在调用了fork后,这两个进程(父进程和子进程)拥有相同的存储映像、同样的环境字符串和同样的打开文件。这就是全部情形。通常,子进程接着执行exec()或一个类似的系统调用,以修改其存储映像并运行一个新的程序。

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

通常,在系统调用fork()之后,有个进程使用系统调用exec(),以用新程序来取代进程的内存空间。系统调用exec()加载二进制文件到内存中(破坏了包含系统调用exec()的原来程序的内存内容),并开始执行。

父进程能够创建更多子进程,或者如果在子进程运行时没有什么可做.那么它采用系统调用wait()把自己移出就绪队列,直到子进程终止。因为调用exec()用新程序覆盖了进程的地址空间,所以调用exec()除非出现错误,不会返回控制。

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

面试基础篇|操作系统|进程

Windows的进程创建

进程创建采用Windows API函数CreateProcess() ,它类似于fork() (这是父进程用于创建子进程的)。不过,fork()让子进程继承了父进程的地址空间,而CreateProcess()在进程创建时要求将一个特定程序加载到子进程的地址空间。

再者,fork()不需要传递任何参数,而CreateProcess()需要传递至少10个参数。其中包括要执行的程序、输入给该程序的命令行参数、各种安全属性、有关打开的文件是否继承的控制位、优先级信息、为该进程(若有的话)所需要创建的窗口规格以及指向一个结构的指针,在该结构中新创建进程的信息被返回给调用者。

总结一下:

在UNIX和Windows中,进程创建之后,父进程和子进程有各自不同的地址空间。

  • 在UNIX中,子进程的初始地址空间是父进程的一个副本,但是这里涉及两个不同的地址空间,不可写的内存区是共享的(某些UNIX的实现使程序正文在两者间共享,因为它不能被修改)。但是,对于一个新创建的进程而言,确实有可能共享其创建者的其他资源,诸如打开的文件等。
  • 在Windows中,从一开始父进程的地址空间和子进程的地址空间就是不同的。

进程不可能一直存在,有进程的创建就必然有进程的终止。

进程终止

引起进程终止的条件也主要有四种:

  1. 正常退出(自愿的)
  2. 出错退出(自愿的)
  3. 严重错误(非自愿)
  4. 被其他进程杀死(非自愿)

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

通常,只有终止进程的父进程才能执行这一系统调用。否则,用户可以任意终止彼此的作业。记住,如果终止子进程,则父进程需要知道这些子进程的标识符。因此,当一个进程创建新进程时,新创建进程的标识符要传递到父进程。

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

  • 子进程使用了超过它所分配的资源。(为判定是否发生这种情况,父进程应有一个机制,以检查子进程的状态)。
  • 分配给子进程的任务,不再需要。
  • 父进程正在退出,而且操作系统不允许无父进程的子进程继续执行。

父进程可以通过系统调用wait(),等待子进程的终止。系统调用wait()可以通过参数,让父进程获得子进程的退出状态;这个系统调用也返回终止子进程的标识符,这样父进程能够知道哪个子进程已经终止了 。

当一个进程终止时,操作系统会释放其资源。不过,它位于进程表中的条目还是在的,直到它的父进程调用wait() ;这是因为进程表包含了进程的退出状态。

当进程已经终止,但是其父进程尚未调用wait(),这样的进程称为僵尸进程(zombie process)。

所有进程终止时都会过渡到这种状态,但是一般而言僵尸进程只是短暂存在一旦父进程调用了 wait(),僵尸进程的进程标识符和它在进程表中的条目就会释放。

如果父进程没有调用wait()就终止以致于子进程成为孤儿进程(orphan process)。

Linux和UNIX对这种情况的处理是:将init进程作为孤儿进程的父进程。进程init定期调用wait(),以便收集任何孤儿进程的退出状态,并释放孤儿进程标识符和进程表目。

进程从创建到终止的过程中,其状态可能会不断的改变,在各种状态之间相互切换。

进程状态

进程主要的5种状态:

  1. 运行状态(Running)

    进程正在处理机上运行。

  2. 就绪状态(Ready)

    进程获得了除了处理机之外的所有所需的资源,得到处理机即可运行。

  3. 阻塞状态(Block)

    进程正在等待某一事件的出现而暂停运行。

  4. 创建状态(New)

    一个进程正在被创建,还没被转到就绪状态之前的状态,是一个过渡状态。也就是在分配资源和相应的数据结构

  5. 退出状态(Exit)

    一个进程反正在从系统中消失时的状态,这是因为进程结束或者由于其他原因所致(也就是系统正在回收资源)。

面试基础篇|操作系统|进程

转换1:运行态到阻塞态

运行的进程需要等待I/O或者其他进程的结果作为输入,此时进程自动阻塞。

转换2:运行态到就绪态

一个进程运行的时间片用完或被高优先级的进程抢占。

转换3:就绪态到运行态

轮到就绪进程运行时。

转换4:阻塞态到就绪态

当进程等待的一个外部事件发生时(如一些输入到达)。

当进程从运行态转换为就绪态时,这时候其他的进程在得到了处理机,这个过程发生了进程切换。

进程切换

切换CPU到另一个进程需要保存当前进程状态和恢复另一个进程的状态, 这个任务称为上下文切换(context switch)。当进行上下文切换时, 内核会将旧进程状态保存在其PCB中, 然后加载经调度而要执行的新进程的上下文。

上下文切换的时间是纯粹的开销, 因为在切换时系统并没有做任何有用工作。 上下文切换的速度因机器不同而有所不同, 它依赖于内存速度、 必须复制的寄存器数量、 是否有特殊指令(如加载或存储所有寄存器的单个指令)。典型速度为几毫秒。

所以进程的切换是非常耗时的,浪费了cpu的资源。所以引入了线程的概念。