进程控制

2018/11/29 服务器

进程控制

进程是系统环境的一个基本组成部分,是系统资源的基本单位,UNIX系统中完成的工作几乎通过进程来控制。

进程创建

进程有一个唯一标识PID(正整数)与之关联,创建进程就会获得其PID。

系统创建时就存在的几个特殊进程:

  • PID为0,swapper调度进程;
  • PID为1,init进程,在系统自举过程末尾由内核创建的;
  • PID为2,pagedaemon,负责支持虚拟系统的分页。

特殊的进程在0~n之间,普通用户的进程在 n+1 - MAXPID-1之间,用户PID通常比较大。

getpid()获取PID:

image

getppid()获取调用进程的父进程。

应用程序创建进程的唯一方法是在执行进程中fork新进程。 image

fork函数创建新进程,与当前进程构成父子关系。

若fork调用成功,则同时存在父进程和子进程且二者均从fork返回,但具有不同的返回值:子进程的返回值为0,而父进程返回的是子进程的PID。 fork返回的子进程的PID给父进程的原因:一个进程可以有多个子进程,因而无法通过函数获取到进程的子进程ID。fork返回0给子进程的原因因为每个子进程仅有一个父进程,子进程通过getppid()而获得父进程ID。

若调用失败,fork返回-1,并置errno指出失败原因(如EAGAIN没有足够资源用来创建进程或已经有太多进程在运行)。

当fork成功,父子进程均从fork之后一条语句继续执行,子进程几乎是父进程的复制。

1)共同特征

  • 实际用户ID
  • 有效用户ID
  • 会晤ID
  • 控制终端
  • 当前工作目录
  • 根目录
  • 文件方式创建屏蔽
  • 环境变量
  • 所有相连的共享存储段
  • 资源限制
  • 任何打开的文件描述字的执行时关闭标志FD_CLOEXEC

2)不同点

  • 子进程有自己的唯一进程ID
  • 子进程拥有其父进程打开的文件描述字副本,此副本属于子进程本身,随后父进程改变其文件描述字属性不会影响到子进程,反过来也是一样。
  • 子进程不继承父进程设置的文件锁
  • 子进程不继承父进程设置的定时器
  • 父进程的任何悬挂信号在进程中都被清除,但子进程从父进程继承他的信号屏蔽和信号动作。
  • 子进程已耗费的紧凑时间tms_utime、time_stime、tms_cutime和tms_cstime均置为0

fork函数一般在以下两种情况使用:

  • 当一个进程想要复制自己以便父进程和子进程在同一时刻可以这行不同的代码段时。例如网络服务,父进程调用fork并让子进程处理请求,然后父进程返回并等待下一个服务请求到达。
  • 当一个进行想要执行另一个不同的程序时,shell便是这种情况的典型用法。在这种情况下,子进程在从fork返回后不久执行一个exec函数。

第二种情况遇到资源使用效率的问题:fork调用必须给子进程创建一个逻辑上与父进程不同的地址空间。然而,由于fork返回后立即调用exec,将抛弃这空间,故复制地址空间是对资源极大的浪费。 解决此问题:

  • 写复制。父进程的数据和栈空间临时成为只读的并标志为“写——复制”。子进程一开始时与父进程共享存储页。如果子进程或父进程企图修改某页,缺页中断便出现,从而UNIX内核识别出这是 一个“写——复制”页,于是复制该页成为新的可写页。这样只复制修改的页,而不是整个进程的地址空间。如果子进程调用exec或exit,这些页转换为它们原来的保护并写——复制标志被清除。
  • vfork函数。类似fork,比fork效率更高。
    • vfork所创建的子进程共享其父进程的地址空间,即父进程借它的地址空间给子进程,直到子进程调用exec或exit为止。
    • vfork在出借父进程的地址空间给子进程的同时阻塞父进程,父进程悬挂执行子进程直到调用了exec或exit,此时内核返回地址空间给父进程并唤醒它。

vfork特别快,因为它无需复制地址映射表,地址空间直接通过地址映射寄存器传递给子进程。但它是一种非常危险的调用,因为它允许一个进程使用、甚至修改另一个进程的地址空间。 使用必须小心,不能让其修改任何全局数据,共享局部变量。

执行一个新程序

fork创建一个新进程时,所创建的子进程具有父进程的程序映象,子进程是父进程的克隆。但多数情况下,创建新进程的目的是执行一个新程序,因此子进程克隆的程序映象将以某种方式被新程序的程序映象所替代。

exec函数用于产生一个新的程序映象。 image 6个函数具有相同的功能,都用新程序的程序映象覆盖进程原来的程序映象。新程序文件由参数path或file给出,它的程序代码将替代原来的程序代码被执行。

exec函数调用成功系统将用一个新程序的地址空间替代调用进程的地址空间并装入新程序的内容。如果调用进程由cfork创建,exec返回老的地址空间给父进程;否则它释放老的地址空间。 当exec返回时,进程从新程序(main函数)的第一条指令处开始执行。这意味成功的exec调用决不会返回,因为原来的程序空间已经被新程序的地址空间多覆盖,只有当调用失败时才返回,其返回值为-1并置errno指出错误原因。

exec执行新程序,进程仍保持原样,只是相连的程序被替换。

新程序保持调用进程的下述属性:

  • 进程ID和父进程ID;
  • 实际用户ID和实际组ID;
  • 附加组ID;
  • 会晤期ID和进程组ID;
  • 控制终端;
  • 闹钟定时器中遗留的时间;
  • 当前工作目录和根目录;
  • 文件方式创建屏蔽;
  • 文件锁;
  • 进程信号屏蔽;
  • 悬挂信号
  • 资源限制
  • tms_utime、time_stime、tms_cutime和tms_cstime之值。

新程序中发生变化属性:

  • 调用进程中打开的文件描述字仍保持打开,但设置了执行并关闭标志FD_CLOSECEC的文件描述字除外。
  • 调用进程打开的目录流经过exec后在新映象中被关闭;
  • 在调用进程中器句柄置为默认或为忽略的信号经exec后在新映象中仍为默认或忽略。但调用进程原设置为要捕获 的信号在新映象中的句柄改为默认,并且,exec调用成功后不再保留替代 信号栈且SA_ONSTACK标志被清除。
  • exec调用成功后,先前通过atexit所注册的函数不再是注册。

6个exec之间的关系: image

等待进程完成

父、子进程可以同时进行,相互之间没有等待。Shell在后台开始一新进程时采用就是这种方式。

不过这种方式并不总是希望的,父进程常常需要等待子进程执行完成后才能继续执行。下述函数用于等待进程的终止: image

wait和waitpid函数允许调用进程获取子进程的状态信息。

wait函数首先检查调用进程是否有任何已终止的子进程,如果有的话,它立即返回;如果没有已终止的子进程,wait阻塞调用进程直至有一个子进程终止并在此时立即返回。 如果调用进程没有任何子进程或wait由子信号而被中断,wait将返回-1并置errno指出错误。

waitpid提供wait未提供的特征:

  • waitpid允许等待某个特定的子进程,而wait返回任意一个已终止进程的状态;
  • waitpid提供wait的非阻塞版本;
  • waitpid支持作业控制。

进程终止和僵死进程

已知终止进程有两种方式:正常终止和异常终止。

正常终止

1)从main函数中return。这相当于调用exit; 2)调用exit; 3)调用_exit函数,该函数由exit调用并处理与UNIX相关的细节;

异常终止

1)调用abort函数(生成SIGNBORT信号)。 2)当进程收到某种信号时。信号可以由进程本身产生,也可以由其他进程或内核生成。

不论进程如何终止,内核都会执行相同的代码。

  • 关闭所打开的文件;
  • 释放进程的存储空间和其他资源;
  • 在进程的proc结构中保存资源使用统计和终止状态;
  • 改变进程为SZOMB(僵死状态)并在僵死进程表中放置proc结构;
  • 使init进程接管此终止进程的所有活跃子进程,即init进程成为他们的父进程;
  • 向父进程发送SIGCHLD信号,此信号可以忽略,仅当父进程想知道子进程的死亡时才有效。

僵死进程

为避免遗留僵死进程子系统之中,通常都需调用wait来等待子进程。但有时,由于某种需要,在派生子进程之后不想等待它完成。 这种情况下,避免僵死进程遗留在系统中直到程序终止而常常采用的一种方法是调用fork两次。 image 在第二个子进程内调用sleep以保证打印父进程ID之前第一个子进程终止。 采用技巧是,第一个子进程fork另一个子进程来执行程序并先于第二个子进程终止。由于第一个子进程的父进程调用了waitpid,他不会遗留在系统的僵死进程。而第二个子进程尽管其父进程没有 调用wait等待它,但由于父进程先于它而终止使得它被init所继承。这使得它是活跃的进程,当它终止时,init将调用wait释放其proc结构,因而不会成为僵死进程。

system函数

可以在一个程序内通过system函数来运行另一个程序并创建一个新的进程(一个程序内执行另一个程序)。 image 参数command为要执行的命令字符串,将直接传送给命令解释程序shell,由shell来执行此命令。因为command参数将传递给shell,因此其中可以含有输出重定向,也可以含有管道结构。

实际上system函数是通过fork、waitpid、exec来实现的。首先fork一子进程,由该子进程如下所示调用shell命令解释程序sh来执行命令:

exec( /bin/sh,"sh","-C",command,(char *)0 );

因此system有三种返回值:

  • 如果不能启动sh来运行此命令,system返回127。
  • 如果system调用出现其他错误(fork失败或waitpid返回EINTR错误),则返回-1并置errno指明错误;
  • 调用成功,返回shell的终止状态,此终止状态的形式同waitpid返回的终止状态一致。

存在三个问题:

  • 极不灵活,进程对这些子进程没有控制权;
  • 带来很大开销
  • system存在安全漏洞

进程组

每一个进程除了具有进程ID,也属于某个进程组。进程组是一个或多个进程的集合。每一个进程组由一个唯一的进程组ID 标识,进程组ID简记PGID。

函数getpgrp返回调用进程的进程组ID: image

每一个进程组有一个组长,它是PID与PGID相同的进程。通常,一个进程从他的父进程继承进程组ID,并且在此组内的所有其他进程都是该进程组长的子孙后代。

进程组长可以创建一个进程组、创建进程组内的进程,以及终止他们。不论进程组长是否终止,只要组内还有一个进程,这个进程组就存在。称从进程组被创建开始至组内最后一个进程离开此进程 为止这段时间称为进程组的生命周期。最后离开这个进程组的进程可能因为终止了执行或者进入另一个进程组。

通过调用setpgid函数,进程可以改变它的进程组ID从而加入到一个已经存在的进程组中;或者改变自己的进程组ID等于自身的PID而创建一个新的进程组,从而使自己成为新进程组的组长。 image

属于shell单条命令的这些进程称为一个“进程组”或“作业”,如:

cat myfile.nr|pic|tbl|troff -ms|lp&

(cat pic tbl … )这些进程被安排到同一个进程组里。

会晤期

会晤期是一个或多个进程组的集合。每一个进程属于一个会晤期和一个进程组。由一次注册产生的所有进程属于同一个会晤期。

image

控制终端

每一个会晤期可以有一个控制终端,这通常就是用户注册时所在的终端或伪终端。控制终端又称为注册终端,进程可以通过此控制终端进行输入、输出和控制作业的运行。

控制终端和会晤期、进程组以及进程之间有如下关系:

  • 每一个进程可以有一个控制终端;
  • 每一个会晤期可以有、也可以没有控制终端。
  • 控制终端由子系统在fork调用继承,因此具有控制终端的会晤期的每一个进程具有相同的控制终端。
  • 当会晤期有控制终端时,与此控制终端相连有一个进程组,它是会晤期的前台进程组,故前台控制组也称为控制组。实质上,前台控制组是控制终端的属性。
  • 每当控制终端输入中断键(常为delete或ctrl+c)或者退出键(ctrl+z)时,将导致中断信号(SIGINT)或退出信号(SIGOUT)发送给前台进程组的每一个进程。
  • 如果进程在前台控制组中,它可以不受限制地自由访问控制终端;而当后台进程组中的一个进程试图读控制终端时,通常会向进程组发送-SIGINT信号,正常情况下将导致此进程组中的所有进程 暂停。同样的,当后台进程组中的进程试图写控制终端时,默认行为发送SIGOUT信号给所有进程。
  • 如果终端驱动程序检测到一个断开的终端连接,它将发送一个SIGHUP信号给控制终端(即会晤主席)。如果原会晤期的任何进程仍企图使用此终端的话会产生问题。

控制终端和会晤期、进程组以及进程之间关系: image

作业控制

作业控制指允许用户在单个会晤期内的多个进程组(也称为作业)之间移动的机制。

允许用户同时控制多个进程。用户一般通过由终端I/O驱动程序和命令解释程序共同提交的交互界面来使用作业控制。利用作业控制,用户可以:

  • 挂起一个正在运行的作业;
  • 放置作业于后台执行;
  • 继续(恢复)被挂起作业的执行
  • 是一个后台作业回到前台;
  • 当后台作业企图网终端输出时,使该作业暂停执行;
  • 当后台作业企图网终端输入时,使该作业暂停执行;

在支持作业控制的shell中,用户可以用在前台或后台自动作业。在前台启动时,shell等待改作业的完成,然后再提示输入另外的命令。在后台运行时,shell不等待,而是立即提示输入新的命令。

C shell作业控制命令: image

实现作业控制的shell

shell要提供作业控制: 1)首先具备关键特性将进程分成作业,也就是将进程分成组。为了把信号引导到一个作业,以及为了标识哪一个作业是在前台,这种分组是必要的,因为在任何一个终端,同时只能有一个前台作业。

进程组的概念被用来提供这种分组,故术语‘作业’和‘进程组’可以互换使用。

2) shell必须控制哪个作业当前在前台。 当前台作业暂停或终止时,shell通过tcsetpgrp把它自己放到前台,然后再提示输入另外的命令。注意,当一个作业被创建时,新进程组开始时作为后台进程组的。要把一个进程组移入前台,需要 由shell产生一个显式调用tcsetpgrp的动作。

3)shell必须知道作业中进程的暂停或终止,并能够在前台或后台切换和继续这些作业的执行。

4)shell必须维护正确的终端设置。

5)shell本身可以从一个shell中产生,所以必须采取特殊动作,以保证子shell很好地与其父shell相配合。

shell中用到的数据结构

主要涉及两个数据结构:

  • job类型包含作业有关的信息,作业是通过管道连接在一起的一组子进程;
  • process类型存放单个子进程的信息。

image

Search

    Table of Contents