8.5 exit函数-进程控制

8.5 exit函数进程控制

如同7 . 3节所述,进程有三种正常终止法及两种异常终止法。

(1) 正常终止:

a)     main函数内执行return语句。如在7 . 3节中所述,这等效于调用exit

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

c)     调用_ exit_Exit系统调用函数。此函数由exit调用,它处理UNIX特定的细节。_ exit是由POSIX.1说明的。其目的是为进程提供一种无需运行终止处理程序或信号处理程序而终止的方法。对标准IO流是否进行冲洗,这取决于实现。在UNIX系统中,两个函数是同义的,并不清洗IO流。

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

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

(2) 异常终止:

a)     调用abort。它产生S I G A B RT信号,所以是下一种异常终止的一种特例。

b)     当进程接收到某个信号时。(第1 0章将较详细地说明信号。)进程本身(例如调用abort函数)、其他进程和内核都能产生传送到某一进程的信号。例如,进程越出其地址空间访问存储单元,或者除以0,内核就会为该进程产生相应的信号。

c)     最后一个线程对取消cancellation请求做出响应。按系统默认,取消以延迟方式发生:一个线程要求取消另一个线程,一段时间以后,目标线程终止。

 

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

对上述任意一种终止情形,我们都希望终止进程能够通知其父进程它是如何终止的。对于exit_ exit,这是依靠传递给它们的退出状态( exit status)参数来实现的。在异常终止情况,内核(不是进程本身)产生一个指示其异常终止原因的终止状态( termination status)。在任意一种情况下,该终止进程的父进程都能用waitwaitpid函数(在下一节说明)取得其终止状态。

注意,这里使用了“退出状态”(它是传向exit_ exit的参数,或main的返回值)和“终止状态”两个术语,以表示有所区别。在最后调用_ exit时内核将其退出状态转换成终止状态(回忆图7 – 1)。下一节中的表8 – 1说明了父进程检查子进程的终止状态的不同方法。如果子进程正常终止,则父进程可以获得子进程的退出状态。

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

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

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

8.4 vfork函数-进程控制

8.4 vfork函数进程控制

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

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

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

实例

在程序8 – 1中使用vfork代替fork,并做其他相应修改得到程序8 – 2。删除了对于标准输出的write调用,另外,我们也不再需要让父进程调用sleepvfork已保证在子进程调用execexit之前,内核会使得父进程处于休眠状态。

 

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>



int glob = 6; /* external variable in initialized data */



int main(void)

{

int var; /* automatic variable on the stack */

pid_t pid;



var = 88;

printf("before vforkn"); /* we don't flush stdio */

if ((pid = vfork()) < 0) {

perror("vfork error");

} else if (pid == 0) { /* child */

glob++; /* modify parent's variables */

var++;

_exit(0); /* child terminates */

}

/*

* Parent continues here.

*/

printf("pid = %d, glob = %d, var = %dn", getpid(), glob, var);

exit(0);

}

运行该程序得到:

$ a.out

before vfork

pid = 2777, glob = 7, var = 89

子进程对变量globvar做增1操作,结果改变了父进程中的变量值因为子进程在父进程的地址空间中运行,所以这并不令人惊讶。但是其作用的确与fork不同。

注意,在程序8 – 2中,调用了_ exit而不是exit。正如8 . 5节所述,_ exit并不执行标准I / O缓存的刷新操作。如果用exit而不是_ exit,则该程序的输出是不确定的。它依赖于标准IO库的实现。可能什么也不输出:

$ a.out

before vfork

从中可见,父进程printf的输出消失了。其原因是子进程调用了exit,它刷新开关闭了所有标准I / O流,这包括标准输出。虽然这是由子进程执行的,但却是在父进程的地址空间中进行的,所以所有受到影响的标准I/O FILE对象都是在父进程中的。当父进程调用printf时,标准输出已被关闭了,于是printf返回– 1

8.3 fork函数-进程控制

8.3 fork函数进程控制

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

#include <unistd.h> 
pid_t fork(void);//返回值:子进程中返回0,父进程中返回子进程ID。出错返回-1

.

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

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

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

      下述程序8-1演示了fork函数,从中可以看到子进程对变量所作的改变并不影响父进程中改变量的值。

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>



int glob = 6; /* external variable in initialized data */

char buf[] = "a write to stdoutn";



int main(void)

{

int var; /* automatic variable on the stack */

pid_t pid;



var = 88;

if (write(STDOUT_FILENO, buf, sizeof(buf) - 1) != sizeof(buf) - 1)

perror("write error");

printf("before forkn"); /* we don't flush stdout */



if ((pid = fork()) < 0) {

perror("fork error");

} else if (pid == 0) { /* child */

glob++; /* modify variables */

var++;

} else {

sleep(2); /* parent */

}



printf("pid = %d, glob = %d, var = %dn", getpid(), glob, var);

exit(0);

}

如果执行此程序则得到:

$ a.out

a write to stdout

before fork

pid = 430, glob = 7, var = 89 子 进 程 的变量值改变了

pid = 429, glob = 6, var = 88 父 进 程 的变量值没有改变

$ a.out > temp.out

$ cat temp.out

a write to stdout

before fork

pid = 432, glob = 7, var = 89

before fork

pid = 431, glob = 6, var = 88

一般来说,在fork之后是父进程先执行还是子进程先执行是不确定的。这取决于内核所使用的调度算法。如果要求父、子进程之间相互同步,则要求某种形式的进程间通信。在程序8 – 1中,父进程使自己睡眠2秒钟,以此使子进程先执行。但并不保证2秒钟已经足够,在8 . 9节说明竟争条件时,还将谈及这一问题及其他类型的同步方法。在1 0 . 6节中,在fork之后将用信号使父、子进程同步。

当写到标准输出时,我们将buf的长度减去1作为输出字节数,这是为了避免将终止null字节写出。strlen计算不包括终止null字节的字符串长度,而sizeof则计算包括终止null字节的缓冲区长度。另一个两者之间的差别是,使用strlen需进行一次函数调用,而对于sizeof而言,因为缓冲区已用已知字符串进行了初始化,其长度是固定的,所以sizeof在编译时计算缓冲区的长度。

注意,程序8 – 1forkI / O函数之间的关系。回忆第3章中所述,w r i t e函数是不带缓存的。因为在fork之前调用w r i t e,所以其数据写到标准输出一次。但是,标准I / O库是带缓存的。回忆一下5 . 1 2节,如果标准输出连到终端设备,则它是行缓存的,否则它是全缓存的。当以交互方式运行该程序时,只得到printf输出的行一次,其原因是标准输出缓存由新行符刷新。但是当将标准输出重新定向到一个文件时,却得到printf输出行两次。其原因是,fork之前调用了printf一次,但当调用fork时,该行数据仍在缓存中(这就是为什么打印到文件中会出现两次before fork的原因),然后在父进程数据空间复制到子进程中时,该缓存数据也被复制到子进程中。于是那时父、子进程各自有了带该行内容的缓存。在exit之前的第二个printf将其数据添加到现存的缓存中。当每个进程终止时,其缓存中的内容被写到相应文件中。

文件共享

对程序8 – 1需注意的另一点是:在重新定向父进程的标准输出时,子进程的标准输出也被

重新定向。实际上, fork的一个特性是所有由父进程打开的描述符都被复制到子进程中。父、子进程每个相同的打开描述符共享一个文件表项(见图3 – 3 )

考虑下述情况,一个进程打开了三个不同文件,它们是:标准输入、标准输出和标准出错。在从fork返回时,我们有了如图8 – 1中所示的安排。

这种共享文件的方式使父、子进程对同一文件使用了一个文件位移量。考虑下述情况:一个进程fork了一个子进程,然后等待子进程终止。假定,作为普通处理的一部分,父、子进程都向标准输出执行写操作。如果父进程使其标准输出重新定向(很可能是由shell实现的),那么子进程写到该标准输出时,它将更新与父进程共享的该文件的位移量。在我们所考虑的例子中,当父进程等待子进程时,子进程写到标准输出;而在子进程终止后,父进程也写到标准输出上,并且知道其输出会添加在子进程所写数据之后。如果父、子进程不共享同一文件位移量,这种形式的交互就很难实现。

clip_image002

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

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

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

(2) 父、子进程各自执行不同的程序段。在这种情况下,在fork之后,父、子进程各自关闭它们不需使用的文件描述符,并且不干扰对方使用的文件描述符。这种方法是网络服务进程中经常使用的。

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

实际用户I D、实际组I D、有效用户I D、有效组I D

添加组I D

进程组I D

对话期I D

控制终端。

设置用户– I D标志和设置– I D标志。

当前工作目录。

根目录。

文件方式创建屏蔽字。

信号屏蔽和排列。

对任一打开文件描述符的在执行时关闭标志。

环境。

连接的共享存储段。

资源限制。

父、子进程之间的区别是:

• fork的返回值。

进程I D

不同的父进程I D

子进程的t m s _ u t i m e , t m s _ s t i m e , t m s _ c u t i m e以及t m s _ u s t i m e设置为0

父进程设置的锁,子进程不继承。

子进程的未决告警被清除。

子进程的未决信号集设置为空集。

其中很多特性至今尚末讨论过,我们将在以后几章中对它们进行说明。

使fork失败的两个主要原因是( a )系统中已经有了太多的进程(通常意味着某个方面出了问题),或者( b )实际用户I D的进程总数超过了系统限制。回忆表2 – 7,其中C H I L D _ M A X规定了每个实际用户I D在任一时刻可具有的最大进程数。

fork有两种用法:

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

(2) 一个进程要执行一个不同的程序。这对shell是常见的情况。在这种情况下,子进程在从fork返回后立即调用exec (我们将在8 . 9节说明exec )

某些操作系统将( 2 )中的两个操作( fork之后执行exec )组合成一个,并称其为s p a w nUNIX将这两个操作分开,因为在很多场合需要单独使用fork,其后并不跟随exec。另外,将这两个操作分开,使得子进程在forkexec之间可以更改自己的属性。例如I / O重新定向、用户I D、信号排列等。在第1 5章中有很多这方面的例子。

8.2 进程标识

8.2 进程标识

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

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

系统中有一些专用的进程:进程ID 0是调度进程,常常被称为交换进程( swapper)。该进程并不执行任何磁盘上的程序—它是内核的一部分,因此也被称为系统进程。进程ID 1通常是init进程,在自举过程结束时由内核调用。该进程的程序文件在U N I X的早期版本中是/ etc / init,在较新版本中是/ sbin / init。此进程负责在内核自举后起动一个U N I X系统。init通常读与系统有关的初始化文件( /etc/rc*文件),并将系统引导到一个状态(例如多用户)init进程决不会终止。它是一个普通的用户进程(与交换进程不同,它不是内核中的系统进程),但是它以超级用户特权运行。本章稍后部分会说明init如何成为所有孤儿进程的父进程。

在某些U N I X的虚存实现中,进程ID 2是页精灵进程( pagedaemon )此进程负责支持虚存系统的请页操作。与交换进程一样,页精灵进程也是内核进程。

除了进程ID,每个进程还有一些其他标识符。下列函数返回这些标识符。

#include <sys/types.h>

#include <unistd.h>

pid_t getpid(void); //返回:调用进程的进程ID

pid_t getppid(void); //返回:调用进程的父进程ID

uid_t getuid(void); //返回:调用进程的实际用户ID

uid_t geteuid(void); //返回:调用进程的有效用户ID

gid_t getgid(void); //返回:调用进程的实际组ID

gid_t getegid(void); // 返回:调用进程的有效组ID

注意,这些函数都没有出错返回,在下一节中讨论f o r k函数时,将进一步讨论父进程ID4 . 4节中已讨论了实际和有效用户及组ID

7 . 11 getrlimit和setrlimit函数

7 . 11 getrlimitsetrlimit函数

每个进程都有一组资源限制,其中某一些可以用getrlimitsetrlimit函数查询和更改。

#include <sys/time.h>

#include <sys/resource.h>

int getrlimit(int resource, struct rlimit * rlptr) ;

int setrlimit(int resource, const struct rlimit * rlptr) ;

//两个函数返回:若成功则为0,若出错则为非0

      这两个函数在Single UNIX Specification中定义为XSI扩展。进程的资源限制通常是在系统初始化时由进程0建立的,然后由每个后续进程继承,没有实现都可以用自己的方法对各种限制做出调整。

对这两个函数的每一次调用都指定一个资源以及一个指向下列结构的指针。

struct rlimit {

rlim_t rlim_cur; /* soft limit: current limit */

rlim_t rlim_max; /* hard limit: maximum value for rlim_cur */

} ;

在更改资源限制时,须遵循下列三条规则:

(1) 任何一个进程都可将一个软限制更改为小于或等于其硬限制

(2) 任何一个进程都可降低其硬限制值,但它必须大于或等于其软限制值。这种降低,对普通用户而言是不可逆反的。

(3) 只有超级用户可以提高硬限制。

一个无限量的限制由常数R L I M _ I N F I N I T Y指定。

这两个函数的resource参数取下列值之一。注意并非所有资源限制都受到SVR44 . 3 + BSD的支持。

• RLIMIT_AS   进程可用存储区的最大总长度(字节),这会影响sbrk函数和mmap函数;

• RLIMIT_CORE    c o r e文件的最大字节数,若其值为0则阻止创建c o r e文件。

• RLIMIT_CPU      CPU时间的最大量值(),当超过此软限制时,向该进程发送S I G X CPU信号。

• RLIMIT_DATA    数据段的最大字节长度。

• RLIMIT_FSIZE     可以创建的文件的最大字节长度。当超过此软限制时,则向该进程发送S I G X F S Z信号。

• RLIMIT_LOCKS 一个进程可持有的文件锁的最大数。

• RLIMIT_MEMLOCK 锁定在存储器地址空间(尚未实现)

• RLIMIT_NOFILE 每个进程能打开的最多文件数。更改此限制将影响到s y s c o n f函数在参数_ S C _ O P E N _ M A X中返回的值。

• RLIMIT_NPROC       每个实际用户I D所拥有的最大子进程数。更改此限制将影响到s y s c o n f函数在参数_ S C _ C H I L D _ M A X中返回的值。

• RLIMIT_OFILE SVR4R L I M I T _ N O F I L E相同。

• RLIMIT_RSS 最大驻内存集字节长度(R S S)。如果物理存储器供不应求,则内核将从进程处取回超过R S S的部分。

• RLIMIT_STACK 栈的最大字节长度。

• RLIMIT_VMEM 可映照地址空间的最大字节长度。这影响到m m a p函数。

资源限制影响到调用进程并由其子进程继承。这就意味着为了影响一个用户的所有后续进程,需将资源限制设置构造在shell之中。确实,Bourne shellKorn Shell具有内部ulimit命令,C shell具有内部l i m i t命令。( umaskchdir函数也必须是shell内部的。)

实例

程序7 – 8打印由系统支持的对所有资源的当前软限制和硬限制。为了在各种实现上编译该程序,我们已经有条件地包括了各种不同的资源名,另请注意,有些平台定义rlim_tunsigned long long而非unsigned long,对于此种平台,必须使用不同的printf格式。

#include <stdio.h>

#include <stdlib.h>



#if defined(BSD) || defined(MACOS)

#include <sys/time.h>

#define FMT "%10lld "

#else

#define FMT "%10ld "

#endif



#include <sys/resource.h>



#define doit(name) pr_limits(#name,name)



static void pr_limits(char *, int);



int main(void)

{

#ifdef RLIMIT_AS

doit(RLIMIT_AS);

#endif



doit(RLIMIT_CORE);

doit(RLIMIT_CPU);

doit(RLIMIT_DATA);

doit(RLIMIT_FSIZE);



#ifdef RLIMIT_LOCKS

doit(RLIMIT_LOCKS);

#endif



#ifdef RLIMIT_MEMLOCK

doit(RLIMIT_MEMLOCK);

#endif



doit(RLIMIT_NOFILE);



#ifdef RLIMIT_NPROC

doit(RLIMIT_NPROC);

#endif



#ifdef RLIMIT_RSS

doit(RLIMIT_RSS);

#endif



#ifdef RLIMIT_SBSIZE

doit(RLIMIT_SBSIZE);

#endif



doit(RLIMIT_STACK);



#ifdef RLIMIT_VMEM

doit(RLIMIT_VMEM);

#endif



exit(0);

}



static void pr_limits(char *name, int resource)

{

struct rlimit limit;

if (getrlimit(resource, &limit) < 0)

perror("getrlimit error");

printf("%-14s ", name);

if (limit.rlim_cur == RLIM_INFINITY)

printf("(infinite) ");

else

printf(FMT, limit.rlim_cur);

if (limit.rlim_max == RLIM_INFINITY)

printf("(infinite)");

else

printf(FMT, limit.rlim_max);

putchar((int) 'n');

}

注意,在doit宏中使用了新的ANSI C字符串创建算符( # ),以便为每个资源名产生字符串值。例如:

doit ( RLIMIT_CORE ) ;

这将由C预处理程序扩展为:

pr_limits(“RLIMIT_CORE”, RLIMIT_CORE);

Ubuntu上运行该程序得到:

RLIMIT_AS       (infinite)  (infinite)

RLIMIT_CORE              0 (infinite)

RLIMIT_CPU      (infinite)  (infinite)

RLIMIT_DATA     (infinite)  (infinite)

RLIMIT_FSIZE    (infinite)  (infinite)

RLIMIT_LOCKS    (infinite)  (infinite)

RLIMIT_MEMLOCK       65536      65536

RLIMIT_NOFILE         1024       1024

RLIMIT_NPROC    (infinite)  (infinite)

RLIMIT_RSS      (infinite)  (infinite)

RLIMIT_STACK       8388608 (infinite)