【APUE笔记】第八章 进程控制

1.fork函数

  功能:一个现存进程调用fork函数是UNIX内核创建一个新进程的唯一方法(这并不适用于前节提及的交换进程、init进程和页精灵进程。这些进程是由内核作为自举过程的一部分以特殊方式创建的)。

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

  子进程和父进程继续执行fork之后的指令。子进程是父进程的复制品。例如,子进程获得父进程数据空间、堆和栈的复制品。注意,这是子进程所拥有的拷贝。父、子进程并不共享这些存储空间部分。如果正文段是只读的,则父、子进程共享正文段

  现在很多的实现并不做一个父进程数据段和堆的完全拷贝,因为在fork之后经常跟随着exec。作为替代,使用了在写时复制(Copy-On-Write, COW)的技术。这些区域由父、子进程共享,而且内核将它们的存取许可权改变为只读的。如果有进程试图修改这些区域,则内核为有关部分,典型的是虚存系统中的“页”,做一个拷贝。

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

fork之后父、子进程之间对打开文件的共享:

  如果父、子进程写到同一描述符文件,但又没有任何形式的同步(例如使父进程等待子进程),那么它们的输出就会相互混合(假定所用的描述符是在fork之前打开的)。虽然这种情况是可能发生的,但这并不是常用的操作方式。

  在fork之后处理文件描述符有两种常见的情况:
(1) 父进程等待子进程完成。在这种情况下,父进程无需对其描述符做任何处理。当子进程终止后,它曾进行过读、写操作的任一共享描述符的文件位移量已做了相应更新。
(2) 父、子进程各自执行不同的程序段。在这种情况下,在fork之后,父、子进程各自关闭它们不需使用的文件描述符,并且不干扰对方使用的文件描述符。这种方法是网络服务进程中经常使用的。

除了打开文件之外,很多父进程的其他性质也由子进程继承:
• 实际用户ID、实际组ID、有效用户ID、有效组ID。
• 添加组ID。
• 进程组ID。
• 对话期ID。
• 控制终端。
• 设置-用户- ID标志和设置-组- ID标志。
• 当前工作目录。
• 根目录。
• 文件方式创建屏蔽字。
• 信号屏蔽和排列。
• 对任一打开文件描述符的在执行时关闭标志。
• 环境。
• 连接的共享存储段。
• 资源限制。

父、子进程之间的区别是:
• fork的返回值。
• 进程ID。
• 不同的父进程ID。
• 子进程的tmsutime, tmsstime,tmscutime以及tmsustime设置为0。
• 父进程设置的锁,子进程不继承。
• 子进程的未决告警被清除。
• 子进程的未决信号集设置为空集。
  fork有两种用法:
(1) 一个父进程希望复制自己,使父、子进程同时执行不同的代码段。这在网络服务进程中是常见的——父进程等待委托者的服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求。
(2) 一个进程要执行一个不同的程序。这对shell是常见的情况。在这种情况下,子进程在从fork返回后立即调用exec。

fork函数的常用使用场景:
1.父进程希望子进程复制自己,使父进程和子进程同时执行不同的代码段。这在网络服务进程中是常见的,父进程等待客户端的服务请求,当接收到客户端发送的请求事件后,调用fork()创建一个子进程,使子进程去处理此请求、而父进程可以继续等待下一个服务请求。
2.一个进程要执行不同的程序。譬如在程序app1中调用fork()函数创建了子进程,此时子进程是要去执行另一个程序app2,也就是子进程需要执行的代码是app2程序对应的代码,子进程将从app2程序的main函数开始运行。这种情况,通常在子进程从fork()函数返回之后立即调用exec族函数来实现。

2.vfork函数

  vfork函数的调用序列和返回值与fork相同,但两者的语义不同。

  功能:vfork用于创建一个新进程,而该新进程的目的是exec一个新程序。vfork与fork一样都创建一个子进程,但是它并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec(或exit),于是也就不会存访该地址空间。不过在子进程调用exec或exit之前,它在父进程的空间中运行。这种工作方式在某些UNIX的页式虚存实现中提高了效率(与上节中提及的,在fork之后跟随exec,并采用在写时复制技术相类似)。

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

3.进程的终止

  进程有三种正常终止法及两种异常终止法。
僵尸进程是当子进程比父进程先结束,而父进程又没有回收子进程,释放子进程占用的资源,此时子进程将成为一个僵尸进程。如果父进程先退出 ,子进程被init接管,子进程退出后init会回收其占用的相关资源

3.1正常终止

(a)在main函数内执行return语句。这等效于调用exit。
(b)调用exit函数。此函数由ANSI C定义,其操作包括调用各终止处理程序(终止处理程序在调用atexit函数时登录),然后关闭所有标准I/O流等。因为ANSIC并不处理文件描述符、多进程(父、子进程)以及作业控制,所以这一定义对UNIX系统而言是不完整的。
(c)调用_ exit系统调用函数。此函数由exit调用,它处理UNIX特定的细节。_exit是由POSIX.1说明的。

3.2异常终止

(a)调用abort。它产生SIGABRT信号,所以是下一种异常终止的一种特例。
(b) 当进程接收到某个信号时。进程本身(例如调用abort函数)、其他进程和内核都能产生传送到某一进程的信号。例如,进程越出其地址空间访问存储单元,或者除以0,内核就会为该进程产生相应的信号。

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

  对上述任意一种终止情形,我们都希望终止进程能够通知其父进程它是如何终止的。对于exit和_exit,这是依靠传递给它们的退出状态( exit status)参数来实现的。在异常终止情况,内核(不是进程本身)产生一个指示其异常终止原因的终止状态( termination status)。在任意一种情况下,该终止进程的父进程都能用wait或waitpid函数(在下一节说明)取得其终止状态。
  注意,这里使用了“退出状态”(它是传向exit或_exit的参数,或main的返回值)和“终止状态”两个术语,以表示有所区别。在最后调用_ e x i t时内核将其退出状态转换成终止状态。如果子进程正常终止,则父进程可以获得子进程的退出状态。

  在说明fork函数时,一定是一个父进程生成一个子进程。上面又说明了子进程将其终止状态返回给父进程。但是如果父进程在子进程之前终止,则将如何呢?其回答是对于其父进程已经终止的所有进程,它们的父进程都改变为init进程。我们称这些进程由init进程领养。其操作过程大致是:在一个进程终止时,内核逐个检查所有活动进程,以判断它是否是正要终止的进程的子进程,如果是,则该进程的父进程ID就更改为1(init进程的ID)。这种处理方法保证了每个进程有一个父进程

  另一个我们关心的情况是如果子进程在父进程之前终止,那么父进程又如何能在做相应检查时得到子进程的终止状态呢?对此问题的回答是内核为每个终止子进程保存了一定量的信息,所以当终止进程的父进程调用wait或waitpid时,可以得到有关信息。这种信息至少包括进程ID、该进程的终止状态、以反该进程使用的CPU时间总量。内核可以释放终止进程所使用的所有存储器,关闭其所有打开文件。在UNIX术语中,一个已经终止、但是其父进程尚未对其进行善后处理(获取终止子进程的有关信息、释放它仍占用的资源)的进程被称为僵死进程(zombie)。ps命令将僵死进程的状态打印为Z。如果编写一个长期运行的程序,它fork了很多子进程,那么除非父进程等待取得子进程的终止状态,否则这些子进程就会变成僵死进程。

  最后一个要考虑的问题是:一个由init进程领养的进程终止时会发生什么?它会不会变成一个僵死进程?对此问题的回答是“否”,因为init被编写成只要有一个子进程终止,init就会调用一个wait函数取得其终止状态。这样也就防止了在系统中有很多僵死进程。当提及“一个init的子进程”时,这指的是init直接产生的进程,或者是其父进程已终止,由init 领养的进程。

4.wait和waitpid函数

  当一个进程正常或异常终止时,内核就向其父进程发送SIGCHLD信号。因为子进程终止是个异步事件(这可以在父进程运行的任何时候发生),所以这种信号也是内核向父进程发的异步通知。父进程可以忽略该信号,或者提供一个该信号发生时即被调用执行的函数(信号处理程序)。对于这种信号的系统默认动作是忽略它。调用wait或waitpid的进程可能会:
(1)阻塞(如果其所有子进程都还在运行)。
(2)带子进程的终止状态立即返回(如果一个子进程已终止,正等待父进程存取其终止状态)。
(3)出错立即返回(如果它没有任何子进程)。
  如果进程由于接收到SIGCHLD信号而调用wait,则可期望wait会立即返回。但是如果在一个任一时刻调用wait,则进程可能会阻塞。

  这两个函数的区别是:
(1)在一个子进程终止前,wait使其调用者阻塞,而waitpid 有一选择项,可使调用者不阻塞。
(2) waitpid并不等待第一个终止的子进程—它有若干个选择项,可以控制它所等待的进程。

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

  这两个函数的参数statloc是一个整型指针。如果statloc不是一个空指针,则终止进程的终止状态就存放在它所指向的单元内。如果不关心终止状态,则可将该参数指定为空指针。

  依据传统,这两个函数返回的整型状态字是由实现定义的。其中某些位表示退出状态(正常返回),其他位则指示信号编号(异常返回),有一位指示是否产生了一个core文件等等。

  终止状态用定义在< sys/wait.h >中的各个宏来查看。有三个互斥的宏可用来取得进程终止的原因,它们的名字都以W I F开始。基于这三个宏中哪一个值是真,就可选用其他宏来取得终止状态、信号编号等。

  如果一个进程有几个子进程,那么只要有一个子进程终止, w a i t就返回。
  如果要等待一个指定的进程终止(如果知道要等待进程的ID ),那么该如何做呢?在早期的UNIX版本中,必须调用wait,然后将其返回的进程ID和所期望的进程ID相比较。如果终止进程不是所期望的,则将该进程ID和终止状态保存起来,然后再次调用wait。反复这样做直到所期望的进程终止。下一次又想等待一个特定进程时,先查看已终止的进程表,若其中已有要等待的进程,则取有关信息,否则调用wait。其实,我们需要的是等待一个特定进程的函数。POSIX.1定义了waitpid函数以提供这种功能(以及其他一些功能)。

对于waitpid的pid参数的解释与其值有关:
• pid == -1 等待任一子进程。于是在这一功能方面waitpid与wait等效。
• pid > 0 等待其进程ID与pid相等的子进程。
• pid == 0 等待其组ID等于调用进程的组ID的任一子进程。
• pid < -1 等待其组ID等于pid的绝对值的任一子进程。

  waitpid返回终止子进程的进程ID,而该子进程的终止状态则通过参数statloc返回。对于wait,其唯一的出错是调用进程没有子进程(函数调用被一个信号中断时,也可能返回另一种出错)。但是对于waitpid,如果指定的进程或进程组不存在,或者调用进程没有子进程都能出错。

waitpid函数提供了wait函数没有提供的三个功能:
(1) waitpid等待一个特定的进程(而wait则返回任一终止子进程的状态)。
(2) waitpid提供了一个wait的非阻塞版本。有时希望取得一个子进程的状态,但不想阻塞。
(3) waitpid支持作业控制。

5.wait3和wait4函数

  功能:它们提供的功能比POSIX.1函数wait、waitpid和waitid所提供的功能要多一个,这与附加参数rusage有关。该参数要求内核返回由终止进程及其所有子进程使用的资源汇总。

6.exec函数

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

相关函数:
execl()
execv()
execle()
execve()
execlp()
execvp()

在执行exec后,进程I D没有改变。除此之外,执行新程序的进程还保持了原进程的下列特征:
• 进程ID和父进程ID。
• 实际用户ID和实际组ID。
• 添加组ID。
• 进程组ID。
• 对话期ID。
• 控制终端。
• 闹钟尚余留的时间。
• 当前工作目录。
• 根目录。
• 文件方式创建屏蔽字。
• 文件锁。
• 进程信号屏蔽。
• 未决信号。
• 资源限制。
• tms_utime, tms_stime, tms_cutime以及tmsustime值。

六个exec函数之间的关系:

7.更改用户ID和组ID

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

关于谁能更改ID有若干规则。现在先考虑有关改变用户ID的规则(在这里关于用户ID所说明的一切都适用于组ID)。
(1) 若进程具有超级用户特权,则setuid函数将实际用户ID、有效用户ID,以及保存的设置-用户-ID设置为uid。
(2) 若进程没有超级用户特权,但是uid等于实际用户ID或保存的设置-用户- ID,则setuid只将有效用户ID设置为uid。不改变实际用户ID和保存的设置-用户- ID。
(3) 如果上面两个条件都不满足,则errno设置为EPERM,并返回出错。

  关于内核所维护的三个用户ID,还要注意下列几点:
(1) 只有超级用户进程可以更改实际用户ID。通常,实际用户ID是在用户登录时,由login程序设置的,而且决不会改变它。因为login是一个超级用户进程,当它调用setuid时,设置所有三个用户ID。
(2) 仅当对程序文件设置了设置-用户- ID位时,exec函数设置有效用户ID。如果设置-用户- ID位没有设置,则exec函数不会改变有效用户ID,而将其维持为原先值。任何时候都可以调用setuid,将有效用户ID设置为实际用户ID或保存的设置-用户- ID。自然,不能将有效用户ID设置为任一随机值。
(3) 保存的设置-用户- ID是由exec从有效用户ID复制的。在exec按文件用户ID设置了有效用户ID后,即进行这种复制,并将此副本保存起来。

  改变三个用户ID的不同方法:

注:getuid和geteuid函数只能获得实际用户ID和有效用户ID的当前值。我们不能获得所保存的设置-用户- ID的当前值。

8.setreuid和setregid函数

  功能:交换实际用户ID和有效用户ID的值。

9.seteuid和setegid函数

  它们只更改有效用户ID和有效组ID。

设置不同的用户ID的各函数:
from APUE

10.system函数

  功能:在程序中执行一个命令字符串。
  使用system而不是直接使用fork和exec的优点是:system进行了所需的各种出错处理,以及各种信号处理。

11.acct函数

  功能:切换或关闭进程记帐。

12.用户标识

  任一进程都可以得到其实际和有效用户ID及组ID。但是有时希望找到运行该程序的用户的登录名。我们可以调用getpwuid (getuid()),但是如果一个用户有多个登录名,这些登录名又对应着同一个用户ID,那么又将如何呢?(一个人在口令文件中可以有多个登录项,它们的用户ID相同,但登录shell则不同。)系统通常保存用户的登录名,用getlogin函数可以存取此登录名。

  如果调用此函数的进程没有连接到用户登录时所用的终端,则本函数会失败。通常称这些进程为精灵进程(daemon),得到了登录名,就可用getpwnam在口令文件中查找相应记录以确定其登录shell等。

13.进程时间

  任一进程都可调用times函数以获得它自己及终止子进程的墙上时钟时间、用户CPU时间和系统CPU时间。
  此函数填写由buf指向的tms结构,该结构定义如下:
from APUE
  注意,此结构没有包含墙上时钟时间。作为代替,times函数返回墙上时钟时间作为函数值。此值是相对于过去的某一时刻度量的,所以不能用其绝对值而必须使用其相对值。例如,调用times,保存其返回值。在以后某个时间再次调用times,从新返回的值中减去以前返回的值,此差值就是墙上时钟时间。(一个长期运行的进程可能其墙上时钟时间会溢出,当然这种可能性极小。)
  结构中两个针对子进程的字段包含了此进程已等待到的各子进程的值。
  所有由此函数返回的clockt值都用_SC_CLK_TCK (由sysconf函数返回的每秒时钟滴答数)变换成秒数。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值