进程控制

8 进程控制

8.1 简介

进程控制,主要包括创建新进程、执行程序和进程终止。此外,还包括进程属性的各种ID——实际、有效和保存的用户ID和组ID,以及她们如何受进程控制原语的影响。也包括解释器文件和system函数,进程会计机制等。

8.2 进程标识

每个进程都有一个非负整数标识的唯一进程ID。因为进程ID标识符总是唯一的,常将其用于其他标识符的一部分以保证其唯一性。如应用程序有时就把进程ID作为名字的一部分来创建一个唯一的文件名。

虽然是唯一的,但是进程ID是可复用的。当一个进程终止后,其进程ID就成为复用的候选者。大多数UNIX系统实现延迟复用算法,使得赋予新建进程的ID不同于最近终止进程所使用的ID。这防止了将新进程误认为使用同一ID的某个已终止的先前进程。

系统中有一些专用进程,但具体细节随实现而不同。ID为0的进程通常是调度进程,常常被称为交换进程(swapper)。该进程是内核的一部分,它并不执行任何磁盘上的程序,因此也被称为系统进程。进程ID 1通常是init进程,在自举过程结束时由内核调用。该进程的程序文件在UNIX的早期版本中是/etc/init,在较新版本中是/sbin/init。此进程负责在自举内核后启动一个UNIX系统。Init通常读取与系统有关的初始化文件(/etc/rc*文件或/etc/inittab文件,以及在/etc/init.d中的文件),并将系统引导到一个状态(如多用户)。Init进程决不会终止。它是一个普通的用户进程(与交换进程不同,它不是内核中的系统进程),但是它以超级用户特权运行。Init可以成为所有孤儿进程的父进程。

每个UNIX系统实现都有它自己的一套操作系统服务的内核进程。如在某些UNIX的虚拟存储器实现中,进程ID 2是页守护进程,此进程负责支持虚拟存储器系统的分页操作。

处理进程ID,每个进程还有一些其他标识符,如父进程ID,使用用户ID,有效用户ID,实际组ID,有效组ID等。

8.3 函数fork

一个现有的进程可以调用fork函数创建一个新进程。

由fork创建的新进程被称为子进程(child process)。Fork函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是0,而父进程的返回值则是新建子进程的进程ID。将子进程ID返回给父进程的理由是:因为一个进程的子进程可以有多个,并且没有一个函数使得一个进程可以获得其所有子进程的进程ID。Fork使子进程得到返回值0的理由是:一个进程只会有一个父进程,所有子进程总是可以调用getppid以获的其父进程的进程ID(进程ID 0总是由内核交换进程使用,所以一个子进程的进程ID不可能为0)。

子进程和父进程继续执行fork调用之后的指令。子进程是父进程的副本。如子进程获得父进程数据空间、堆和栈的副本。注意,这是子进程所拥有的副本。父进程和子进程并不共享这些存储空间部分。父进程和子进程共享正文段。

由于在fork之后经常跟随着exec,所以现在的很多实现并不执行一个父进程数据段、堆和栈的完全副本。作为替代,使用了写时复用(Copy-On-Write,COW)技术。这些区域由父进程和子进程共享,而且内核将他们的访问权限改变为只读。这些区域由父进程和子进程共享,而且内核将它们的访问权限改变为只读。如果父进程和子进程中的任一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本,通常是虚拟存储系统中的一“页”。

一般来说,在fork之后是父进程先执行,还是子进程先执行,是不确定的,这取决于内核所使用的调度算法。如果要求父进程和子进程之间相互同步,则要求某种形式的进程间通信。

文件共享

父进程和子进程共享同一个文件偏移量。

在fork之后,处理文件描述符有以下两种常见的情况:

²  父进程等待子进程完成。在这种情况下,父进程无需对其描述符做任何处理。当子进程终止后,它曾进行过读、写操作的任一共享描述符的文件偏移量已做了相应更新。

²  父进程和子进程各自执行不通的程序段。在这种情况下,在fork之后,父进程和子进程各自关闭他们不需要的文件描述符,这样就不会干扰对方使用的文件描述符。这种方法是网络服务进程经常使用的。

除了打开文件之外,父进程的很多其他属性也由子进程继承,包括:

²  实际用户ID,实际组ID,有效用户ID,有效组ID

²  附属组ID

²  进程组ID

²  会话ID

²  控制终端

²  设置用户ID标志和设置组ID标志

²  当前工作目录

²  根目录

²  文件模式创建屏蔽字

²  信号屏蔽和安排

²  对任意打开文件描述符的执行式关闭(close-on-exec)标志

²  环境

²  连接的共享存储段

²  存储映像

²  资源限制

父进程和子进程之间的区别具体如下:

²  Fork的返回值不同

²  进程ID不同

²  这两个进程的父进程ID不同:子进程的父进程ID是创建它的进程ID,而父进程的父进程ID则不变

²  子进程的tms_utime,tms_stime,tms_cutime和tms_ustime的值设置为0

²  子进程不继承父进程设置的文件锁

²  子进程的未处理闹钟被清除

²  子进程的未处理信号集设置为空集

使fork失败的主要原因有两个:

²  系统中已经有了太多的进程(通常意味着某个方面出现了问题)

²  该实际用户ID的进程总数超过了系统限制。

Fork有以下两种用法:

²  一个父进程希望复制自己,使父进程和子进程同时执行不同的代码段。这在网络服务进程中是常见的——父进程等待客户端的服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程继续等待下一个服务请求。

²  一个进程要执行不通的程序。这对shell是常见的情况。在这种情况下,子进程从fork返回后立即调用exec。

某些操作系统将第2种用法中的两个操作(fork之后执行exec)组合成一个操作,称为spawn。UNIX系统将这两个操作分开,因为在很多的场合需要单独使用fork,其后并不跟随exec。另外,将这两个操作分开,使得子进程在fork和exec之间可以更改自己的属性,如I/O重定向,用户ID,信号安排等。

8.4 函数vfork

Vfork函数的调用属性怒和返回值与fork相同,但两者的语义不同。Vfork函数用于创建一个新进程,而该进程的目的是exec一个新程序。Vfork与fork一样都创建一个子进程,但是它并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec(或exit),于是也就不会引用改地址空间。不过在子进程调用exec或exit之前,它在父进程的空间中运行。这种优化工作方式在某些UNIX系统的实现中提高了效率,但如果子进程修改数据(除了用于存放vfork返回值的变量)、进行函数调用、或者没有调用exec或exit就返回都可能会带来未知的结果。(实现采用写时复用技术以提高fork之后跟随exec操作的效率,但是不复制比部分幅值还是要快一些)。

Vfork和fork之间的另一个区别是:vfork保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行,当子进程调用这两个函数的任意一个时,父进程会恢复运行,(如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁)。

8.5 函数exit

5种正常终止方式和3中异常终止方式具体如下:

5种正常终止方式:

²  在main函数内执行return语句。等效于调用exit

²  调用exit函数。此函数由ISO C定义,其操作包括调用各终止处理程序(终止处理程序在调用atexit函数时登记),然后关闭所有标准I/O流。因为ISO C并不处理文件描述符、多进程(父进程和子进程)以及作业控制,所以这一定义对UNIX系统而言是不完整的。

²  调用_exit或_Exit函数。ISO C定义_Exit,其目的是为进程提供一种无需运行终止处理程序或信号处理程序而终止的方法。对标准I/O流是否进行冲洗,这取决于实现。在UNIX系统中,_Exit和_exit是同义的,并不冲洗标准I/O流。_exit函数由exit调用,它处理UNIX系统特定的细节。_exit是由POSIX.1说明。

²  进程的最后一个线程在其启动例程中执行return语句。但是该线程的返回值不用做进程的返回值。当最后一个线程从其启动例程返回时,该进程以终止状态0返回。

²  进程的最后一个线程调用pthread_exit函数。如同前面一样,在这种情况下,进程终止状态总是0,这与传送给pthread_exit的参数无关。

3种异常终止方式:

²  调用aboart。它产生SIGABRT信号,这是下一种异常终止的一种特例

²  当进程接收到某些信号时。信号可有进程自身(如调用aboart函数)、其他进程或内核产生。如若进程引用地址空间之外的存储单元、或者除以0,内核就会为该进程产生相应的信号

²  最后一个线程对“取消”的请求做出响应。默认情况下,“取消”以延迟方式发生:一个线程要求取消另一个线程,若干时间后,目标线程终止。

不管进程如何终止,最后都会执行内核中同一段代码。这段代码为相应进程关闭所有打开描述符,释放它所使用的存储器等。

对上述任意一种情形,我们都希望终止进程能够通知其父进程它是如何终止的。对于3个终止函数(exit,_exit,_Exit),实现这一点的方法是,将其退出状态作为参数传送给函数。在异常终止情况下,内核(不是进程本身)产生一个指示其异常终止原因的终止状态。在任意一种情况下,该终止进程的父进程都能用wait或waitpid函数取得其终止状态。

注意:这里使用了“退出状态”(它是传递给向3个终止函数的参数,或main的返回值)和终止状态两个术语,以表示有所区别。在最后调用_exit时,内核将退出状态转换成终止状态。如果子进程正常终止,则父进程可以获得子进程的退出状态。

父进程在子进程之前终止,则:对于父进程已经终止的所有进程,他们的父进程都改变为init进程,称这些进程由init进程收养。其操作过程如下:在一个进程终止时,内核逐个检查所有活动的进程,以判断他是否是正要终止进程的子进程,如果是,则该进程的父进程ID就改为1(init进程的ID),这种处理方法保证了每个进程有一个父进程。

如果子进程在父进程之前终止,那么父进程如何能在做相应检查时得到子进程的终止状态?

如果子进程完全消失了,父进程在最终准备好检查子进程是否终止时是无法获取它的终止状态的。内核为每个终止子进程保存了一定量的信息,所以当终止进程的父进程调用wait或wiatpid时,可以得到这些信息。这些信息至少包括进程ID、该进程的终止状态以及该进程使用的CPU时间总量。内核可以释放终止进程所使用的所有存储区,关闭其所有打开文件。在UNIX术语中,一个已经终止、但是其父进程尚未对其进行善后处理(获取终止进程的有关信息,释放它扔占用的资源)的进程被称为僵死进程。如果编写一个长期运行的程序,它fork了很多子进程,那么除非父进程等待取得子进程的终止状态,不然这些子进程终止后就会变成僵死进程。

一个由init进程收养的进程终止会发生什么?它会不会变成一个僵死进程?

答案是“否”,因为init被编写成无论何时只要有一个子进程终止,init就会调用一个wait函数取得其终止状态。这样也就防止了在系统中塞满僵死进程。当提及“一个init的子进程“时,指的可能是init直接产生的进程,也可能是其父进程已经终止,由init收养的进程。

8.6 函数wait和waitpid

当一个进程正常或异常终止时,内核就向其父进程发送SIGCHLD信号。因为子进程终止时个异步事件(这可以在父进程运行的任何时候发生),所以这种信号也是内核向父进程发的异步通知。父进程可以选择忽略该信号,或者提供一个该信号发生时即被调用执行的函数(信号处理程序)。对于这种信号的系统动作默认是忽略它。

调用wait和waitpid的进程可能会发生什么?

²  如果其所有子进程都换在运行,则阻塞

²  如果一个子进程已终止,正等待父进程获取其终止状态,则取得该子进程的终止状态立即返回。

²  如果它没有任何子进程,则立即出错并返回

如果进程由于接收到SIGCHLD信号而调用wait,我们期望wait会立即返回。但是如果在随机时间点调用wait,则进程可能会阻塞。

Wait函数和waitpid函数区别如下:

²  在一个子进程终止前,wait使其调用者阻塞,而waitpid有一选项,可使调用者不阻塞

²  Waitpid并不等待在其调用之后的第一个终止子进程,它有若干个选项,可以控制它所等待的进程。

如果子进程已经终止,并且是一个僵死进程,则wait立即返回并取得子进程的状态;否则wait使其调用者阻塞,直到一个子进程终止。如调用者阻塞而且它有多个子进程,则在其某一子进程终止时,wait就立即返回。因为wait返回终止子进程的进程ID,所以它总能了解是那一个子进程终止。

如果要等待一个指定的进程终止(如果知道要等待进程的ID),该如何做?

在早期的UNIX版本中,必须调用wait,然后将其返回的进程ID和所期望的进程ID相比较。如果终止进程不是所期望的,则将该进程ID和终止状态保存起来,然后再此调用wait。反复这样做,知道所期望的进程终止。下一次又想等待一个特定的进程时,先查看终止的进程列表,若其中已有要等待的进程,则获取相关信息;否则调用wait。

Waitpid函数提供了wait函数没有提供的3个功能:

²  Waitpid可等待一个特定的进程,而wait则返回任一终止子进程的状态。

²  Waitpid提供了一个wait的非阻塞版本。有时希望获取一个子进程的状态,但不想阻塞

²  Waitpid通过WUNTRACED和WCONTINUED选项支持作用控制。

8.7 函数waitid

Waitid函数是取得进程终止状态函数。它允许一个进程指定要等待的子进程。但它使用两个单独的参数表是要等待的子进程所属的类型,而不是将此与进程ID或进程组ID组合成一个参数。

8.8 wait3和wait4函数

大多数UNIX系统实现提供了另外两个函数wait3和wait4.历史上,这两个函数是从UNIX系统的BSD分支沿袭下来的。他们提供的功能比POSIX.1函数wait、waitpid和waitid所提供功能的要多一个,这与附加参数有关。该参数允许内核返回由终止进程及其子进程使用的资源概况。

资源统计信息包括用户CPU时间总量、系统CPU时间总量、缺页次数、接收到信号的次数等。

8.9 竞争条件

当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,我们认为发生了竞争条件(race condition)。如何在fork之后的某种逻辑显式或隐式地依赖于在fork之后是父进程先运行的还是子进程先运行,那么fork函数就会是竞争条件活跃的滋生地。通常,我们不能预料哪一个进程先运行。即使知道哪一个进程先运行,在该进程开始运行后,所发生的事情也依赖于系统负载预计内核的调度算法。

如果一个进程要等待其父进程终止,可以使用轮询的方式,但是轮询浪费了CPU时间,因为调用者每间隔1s都被唤醒,然后进行条件测试。

为了避免竞争条件和轮询,在多个进程之间需要有某种形式的信号发送和接收的方法。在UNIX中可以使用信号机制。此外,各种形式的进程间通信(IPC)也可以使用。

在父进程和子进程的关系中,常常出现下述情况。在fork之后,父进程和子进程都有一些事情要做。如父进程可能要用子进程ID更新日志文件中的一个记录,而子进程则可能要为父进程创建一个文件。

8.10 函数exec

当进程调用一种exec函数时,该进程执行的程序完全替换为新程序,而新程序则从其main函数开始执行。因为调用exec并不创建新进程,所以前后的进程ID并未改变。Exec只是用磁盘上的一个新程序替换了当前进程的正文段、数据段、堆段和栈段。

Fork函数用于创建新进程,而exec函数用于初始化执行新的程序。有7种不同的exec函数可供使用。

在执行exec后,进程ID没有改变。但新程序从调用进程继承了下例属性:

²  进程ID和父进程ID

²  实际用户ID和实际组ID

²  附属组ID

²  进程组ID

²  会话ID

²  控制终端

²  闹钟尚余留的时间

²  当前工作目录

²  根目录

²  文件模式创建屏蔽字

²  文件锁

²  进程信号屏蔽字

²  未处理信号

²  资源限制

²  Nice值

²  Tms_utime,tms_stime,tms_cutime,tms_cstime值

对打开文件的处理与每个描述符的执行时关闭(close-on-exec)标志值有关。

注意:在exec前后实际用户ID和实际组ID保持不变,而有效ID是否改变则取决于所执行程序文件的设置用户ID位和设置组ID位是否设置。如果新程序设置了用户ID位,则有效用户ID变成程序文件所有者的ID;否则有效用户ID不变。

8.11 更改用户ID和用户组ID

在UNIX系统中,特权(如能改变当前日期的表示法)以及访问控制(如能够读、写一个特定文件),是基于用户ID和组ID的。当程序需要增加特权,或需要访问当前并不允许访问的资源时,我们需要更改自己的用户ID或组ID,使得新ID具有合适的特权或访问权限。与此类似,当程序需要降低其特权或阻止对某些资源的访问时,也需要更换用户ID或组ID,新ID不具有相应的特权或访问这些资源的能力。

一般而言,在设计应用中,总是试图使用最小特权(least privilege)模型。依照此模型,我们的程序应当只具有为完成给定任务所需的最小特权。这降低了由恶意用户试图哄骗我们程序以及未预料的方式使用特权造成的安全风险。

可以用setuid函数设置实际用户ID和有效用户ID。与此类似,可以用setgid函数设置实际组ID和有效组ID。

更改用户ID的规则:

²  若进程具有超级用户特权,用setuid函数将实际用户ID、有效用户ID以及保存的设置用户ID(savedset-user-ID)设置为uid

²  若进程没有超级用户特权,但是uid等于实际用户ID或保存的设置用户ID,则setuid只将有效用户ID设置为uid。不更改实际用户ID和保存的设置用户ID

²  如果上面条件都不满足,则errno设置为EPERM,并返回-1.

内核所维护的3个用户ID,需要注意以下几点:

²  只有超级用户进程可以更改实际用户ID。通常实际用户ID是在用户登录时,由login程序设置的,而且决不会改变他。因为login是一个超级用户进程,当它调用setuid时,设置所有3个用户ID。

²  仅当对程序文件设置了设置用户ID位时,exec函数才设置有效用户ID。如果设置用户ID位没有设置,exec函数不会改变有效用户ID,而将维持其现有值。任何时候都可以调用setuid,将有效用户ID设置为实际用户ID或保存的设置用户ID。自然地,不能将有效用户ID设置为任一随机值

²  保存的设置用户ID是由exec复制有效用户ID而得到的。如果设置了文件的设置用户ID位,则在exec根据文件的用户ID设置了进程的有效ID以后,这个副本就被保存起来了。

Getuid和geteuid函数只能获得实际用户ID和有效用户ID的当前值。

8.12 解释器文件

所有现今的UNIX系统都支持解释器文件(interpreter file)。这种文件是文本文件,其起始形式为:#! Pathname[ optional-argument] 在感叹号和path之间的空格是可选的最常见的解释器文件以下列行开始:#! /bin/sh。Pathname通常是绝对路径名,对它不进行什么特殊的处理(不使用PATH进行路径搜索)对这种文件的识别是由内核作为exec系统调用处理的一部分来完成,内核使用exec函数的进行实际执行的并不是解释器文件,而是在该解释器文件中第一行pathname所指定的文件。一定要将解释器文件(文本文件,它是#!开头)和解释器(由该解释器文件第一行中的pathname指定)区分开来。

很多系统对解释器文件第一行长度限制。这包括#!、pathname,可选参数,终止换行符以及空格数。

是否一定需要解释器文件呢?那也不完全如此。但是他们确实使用户得到效率方面的好处,其代价是内核的额外开销(因为识别解释器文件的是内核)。由下述理由,解释器文件是有用的。

²  有些程序是用某种语言写的脚本,解释器文件可将这一事实隐藏起来。

²  解释器脚本在效率方面也提供了好处。用一个shell脚本代替解释器脚本需要更多的开销

²  解释器脚本使我们可以使用除/bin/sh以外的其他shell来编写shell甲苯。当execlp找到一个非机器可执行的可执行文件时,它总是调用/bin/sh来解释执行该文件。

8.13 函数system

System函数在其实现中调用了fork、exec和waitpid,故存在3种返回值

²  Fork失败或者waitpid返回除EINTR之外的错误,则system返回-1,并且设置errno以指示错误类型

²  如果exec失败(表示不能执行shell),则其返回值如通shell执行了exit一样。

²  否则所有3个函数(fork,exec,waitpid)都成功,那么system的返回值是shell的终止状态。

使用了system而不是直接使用fork和exec的优点是:system进行了所需的各种出错处理以及各种信号处理。

如果一个程序正以特殊的权限(设置用户ID和设置组ID)运行,它又想生成另一个进程执行另一个程序,则它应当直接使用fork和exec,而且在fork之后,exec之前要更改回普通用户权限。设置用户ID或设置组ID程序决不应调用system函数。

8.14 进程会计

大多数UNIX系统提供了一个选项以进行进程会计处理。启用该选项后,每当进程结束时内核就写一个会计记录。典型的会计记录包含总量较小的二进制数据,一般包括命令名,所使用的CPU时间总量,用户ID和组ID,启动时间等。

会计记录所需的各个数据(各CPU时间,传输的字符数等)都由内核保存在进程表中,并在一个新进程被创建时初始化(如fork之后在子进程中),进程终止时写一个会计记录。这产生两个后果:

²  我们不能获取永远不终止的进程的会计记录。像init这样的进程在系统生命周期中一直在运行,并不产生会计记录。这也同样适合于内核守护进程,他们通常不会终止。

²  在会计文件中记录的顺序对应于进程终止的顺序,而不是他们启动的顺序。

²  会计记录对应于进程而不是程序。在fork之后,内核为子进程初始化一个记录,而不是在一个新程序被执行时初始化。

8.15 用户标识

任一进程都可以得到其实际用户ID和有效用户ID及组ID。但又想得到用户的登录名。使用函数为getpwuid。通常系统记录用户登录时使用的名字,用getlogin函数获取登录名。

8.16 进程调度

UNIX系统历史上对进程提供的只是基于调度优先级的粗粒度的控制。调度策略和调度优先级是由内核确定的。进程可以通过调整nice值选择以更低优选级运行。只有特权进程允许提高调度权限。Nice值越小,优先级越高。进程可以通过nice函数获取或更改它的nice值。Getpriority函数可以像nice函数那样用于获取进程的nice值,但是getpriority还可以获取一组相关的进程nice值。

Setpriority用于为进程、进程组和属于特定用户ID的所有进程设置优先级。

8.17 进程时间

可以度量的3个时间:墙上的时钟时间,用户CPU时间,系统CPU时间。


参考文献:Unix高级环境编程

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值