首页 > 代码库 > APUE学习笔记:第八章 进程控制

APUE学习笔记:第八章 进程控制

8.1 引言

本章介绍UNIX的进程控制,包括创建新进程、执行程序和进程终止。还将说明进程属性的各种ID-----实际、有效和保存的用户和组ID,以及他们如何受到进程控制原语的影响。本章还包括了解释器文件和system函数。本章最后讲述大多数UNIX系统所提供的进程会计机制。这种机制使我们能够从另一个角度了解进程的控制功能。

 

8.2 进程标识符

每个进程都有一个非负整型表示的惟一进程ID。因为进程标识符是惟一的,常将其用作其他标识符的一部分以保证其惟一性。虽然是惟一的,但是进程ID可以重用。(大多数UNIX系统实现延迟重用算法,使得赋予新建进程的ID不同于最近终止进程所使用的ID。这防止了将新进程误认为是使用同一ID的某个已终止的进程。

ID为0通常是系统进程

ID为1通常是init进程

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

#include<unistd.h>pid_t getpid(void);        //返回值:调用进程的进程idpid_t getppid(void);        //返回值:调用父进程的进程IDuid_t getuid(void);        //返回值:调用进程的实际用户iduid_t geteuid(void):            //返回值:调用进程的有效用户idgid_t getid(void)        //返回值:调用进程的实际组idgid_t getegid(void)        //返回值:调用进程的有效组id

这些函数都没有出错返回

 

8.3 fork函数

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

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

将子进程ID返回给父进程的理由是:因为一个进程的子进程可以有多个,并且没有一个函数使一个进程可以获得其所有子进程的进程ID

使子进程得到返回值0的理由是:一个进程只会有一个父进程,所以子进程总是可以调用getppid以获得其父进程的进程ID(进程ID0总是由内核交换进程使用,所以一个子进程的进程ID不可能是0)

 

子进程是父进程的副本,但父、子进程并不共享这些存储空间部分。父子进程共享正文段

由于在fork之后经常跟随者exec,所以现在的很多实现并不执行一个父进程数据段,栈和堆的完全复制。作为替代,使用了写时复制技术。

实例:8_1 fork函数示例

 1 #include"apue.h" 2  3 int glob=6; //external variable in initialized data 4 char buf[]="a write to stdout\n"; 5  6 int main() 7 { 8     int var; //automatic variable on the stack 9     pid_t pid;10     var=88;11     if(write(STDOUT_FILENO,buf,sizeof(buf)-1)!=sizeof(buf)-1)12     err_sys("write error");13     printf("before fork\n");//we don‘t flush stdout14     if((pid=fork())<0){15     err_sys("fork error");16     }else if(pid==0){    //child17     glob++;18     var++;19     }else {sleep(2);20 }21     printf("pid=%d,glob=%d,var=%d\n",getpid(),glob,var);22     exit(0);23 }24     

一般来说,在fork之后是父进程还是子进程先执行是不确定的。这取决于内核的调度算法。8_1中是先让父进程休眠2秒钟,以使子进程先执行

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

在8_1中当将标准输出重定向到一个文件时,却得到printf输出行两次。其原因是,在fork之前调用了printf一次,但当调用fork时,却得到printf输出行两次。其原因是,在fork之前调用了printf一次,但当调用fork时,该行数据仍在缓冲区中,然后在将父进程数据空间复制到子进程中时,该缓冲区也被复制到子进程中,于是那时父、子进程各自有了带该行内容的标准I/O缓冲区。在exit之前的第二个printf将其数据添加到现有的缓冲区中。当每个进程终止时,最终会冲洗其缓冲区的副本

 

父子进程的区别是:

-fork的返回值

-进程ID不同

-两个进程具有不同的父进程ID:子进程的父进程ID是创建它的进程ID,而父进程ID则不变

-子进程的tms_utime,tms_stime,tme_cutime以及tme_ustime均被设置为0

-父进程设置的文件锁不会被子进程继承

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

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

 

使fork失败的两个主要原因是:系统中已经有了太多的进程,或者实际用户ID进程总数超过了系统限制

 

fork有下列两种用法:

(1)一个进程希望复制自己,是父子进程同时执行不同代码段

(2)一个进程要执行一个不同的程序。

 

8.4 vfork函数

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

vfork用于创建一个新进程,而该新进程的目的是exec一个新程序。vfork和fork一样都创建一个子进程,但是它并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec(或exit),于是也就不会存访该地址空间。相反,在子进程调用exec或exit之前,它在父进程的空间中运行。这种优化工作方式在某些UNIX的页式虚拟存储器视线中提高了效率

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

实例:8_2 vfork函数实例

 1 #include"apue.h" 2 int glob=6; 3 int main() 4 { 5     int var; 6     pid_t pid; 7     var=88; 8      9     printf("before vfork\n");10     if((pid=vfork())<0){11     err_sys("vfork error");12     }else if(pid==0){13     glob++;14     var++;15     _exit(0);16     }17     printf("pid=%d,glob=%d,var=%d\n",getpid(),glob,var);18     exit(0);19 }

 

vfork,它产生的子进程刚开始暂时与父进程共享地址空间(其实就是线程的概念了),因为这时候子进程在父进程的地址空间中运行,所以子进程不能进行写操作,并且在儿子“霸占”着老子的房子时候,要
委屈老子一下了,让他在外面歇着(阻塞),一旦儿子执行了exec或者exit后,相当于儿子买了自己的房子了,这时候就相当于分家了。

8.5 exit函数

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

另一个我们关心的情况是如果子进程在父进程之前终止,那么父进程又如何能在做相应检查时得到子程序的终止状态呢?

内核为每个终止子进程保存了一定量的信息,所以当终止进程的父进程调用wait或waitpid,可以得到这些信息,这些信息至少包括进程ID,该进程的终止状态,以及该进程使用的CPU时间总量。内核可以释放终止进程所使用的所有存储区,关闭其所有打开文件。

 

8.6 wait和waitpid函数

#include<sys/wait.h> pid_t wait(int *statloc);pid_t waitpid(pid_t pid,int *statloc,int options);            //两个函数返回值:若成功则返回进程ID,0,若出错则返回-1

这两个函数区别如下:

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

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

实例:8_3 打印exit状态的说明

 1 #include"apue.h" 2 #include<sys/wait.h> 3 void pr_exit(int status) 4 { 5     if(WIFEXITED(status)) 6     printf("normal termination,exit status= %d\n",WEXITSTATUS(status)); 7     else if(WIFSIGNALED(status)) 8     printf("abnormal termination,signal number= %d%s\n",WTERMSIG(status), 9 #ifdef WCOREDUMP 10     WCOREDUMP(status) ? "(core file generated)" : " ");11 #else12     "");13 #endif 14     else if(WIFSTOPPED(status))15     printf("child stopped,signal number= %d\n",WSTOPSIG(status));16 }

实例:8_4 演示不同的exit值

 1 #include"apue.h" 2 #include<sys/wait.h> 3 void pr_exit(int ); 4 int main() 5 { 6     pid_t pid; 7     int status; 8     if((pid=fork())<0) 9     err_sys("fork error");10     else if(pid==0)11     exit(7);12     if(wait(&status)!=pid)13     err_sys("wait error");14     pr_exit(status);15     if((pid=fork())<0)16     err_sys("fork error");17     else if(pid==0)18     abort();19     if(wait(&status)!=pid)20     err_sys("wait error");21     pr_exit(status);22     if((pid=fork())<0)23     err_sys("fork error");24     else if(pid==0)25 //    status/=0;26     if(wait(&status)!=pid)27     err_sys("wait error");28     pr_exit(status);29     exit(0);30 }31 void pr_exit(int  i)32 {33  printf("%d\n",i);34 return;35 }

waitpid函数提供了wait函数没有提供的三个功能:

(1)waitpid可等待一个特定的进程,而wait则返回任一终止子进程的状态。

(2)waitpid提供了一个wait的非阻塞版本。有时用户希望取得一个子进程的状态,但不想阻塞

(3)waitpid支持作业控制

 

8.7 waitid函数

#include<sys/wait.h>int waitid(idtype_t idtype,id_t id,siginfo_t *infop,int options);        //返回值:若成功则返回0,若出错则返回-1

与waitpid相似,waitid允许一个进程指定要等待的子进程。但它使用单独的参数表示要等待的字进程的类型,而不是将此进程ID或进程组ID组合称一个参数

 

8.8wait3 和wait4函数

#include<sys/types.h>#include<sys/wait.h>#include<sys/time.h>#include<sys/resource.h>pid_t wait3(int *statloc,int options,struct rusage *rusage);pid_t wait4(pid_t pid,int *statloc,int options,struct rusage *rusage);        //返回值:若成功则返回进程ID,若出错则返回-1

 

8.9 竞争条件

这部分操作系统原理已经讲的很深了

 

程序清单 8_6 具有竞争条件的程序

 1 #include"apue.h" 2 static void charatatime(char *); 3  4 int main() 5 { 6     pid_t pid; 7     if((pid=fork())<0){ 8     err_sys("fork error"); 9     }else if(pid==0){10     charatatime("output from child\n");11     }else {12     charatatime("output from parent\n");13     }14     exit(0);15 }16 static void charatatime(char *str)17 {18     char *ptr;19     int c;20     setbuf(stdout,NULL);21     for(ptr=str;(c=*ptr++)!=0; )22     putc(c,stdout);23 }

在程序中将标准输出设置为不带缓冲的,于是每个字符输出都需调用一次write.本例的目的是使内核尽可能在两个进程之间进行多次切换,以便演示竞争条件。

 

8.10 exec函数

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

#include<unistd.h>int execl(const char *pathname,const char *arg(),.../*(char *)0*/);int execv(const char *pathname,char *const argv[]);int execle(const char *pathname,const char *arg0,...        /*(char*)0,char *const envp[] */);int execve(const char *pathname,char *const argv[],char *const envp[]);int execlp(const char *filename,const char *arg0,.../*(char*)0*/);int execvp(const char *filename,char *const argv[]);  //返回值:若出错则返回-1,若成功则不返回值

这些函数之间的第一个区别是前4个去路径名作为参数,后两个取文件名作为参数。当指定filename作为参数时:

-如果filename中包含/,则将其视为路径名

-否则就按PATH环境变量,在它所指定的各目录中搜寻可执行文件。

如果execlp或execvp使用路径前缀中的一个找到了一个可执行文件,但是该文件不是由连接编辑器产生的机器可执行文件,则认为该文件是一个shell脚本,于是试着调用/bin/sh,并以该filename作为shell的输入

第二个区别与参数表的传递有关(1表示list,v表示适量vector),函数execl、execlp和execle要求将新进程的每个命令行参数都说明为一个单独的参数。这种参数表以空指针结尾。对于另外三个函数(execv、execvp和execve),则应先构造一个指向各参数的指针数组,然后将该数组地址作为这三个函数的参数

最后一个区别与向新进程传递环境表相关。以e结尾的两个函数(execle和execve)可以传递一个指向环境字符串指针数组的指针。其他四个函数则使用调用进程中的environ变量为新程序复制现有的环境。

注意:在执行exec前后实际用户ID和实际组ID保持不变,而有效ID是否改变则取决于所执行程序文件的设置用户ID位和设置组ID位是否设置。如果新程序的设置用户ID位已设置,则有效用户ID变成程序文件所有者的ID,否则有效用户ID不变。对组ID的处理方式与此相同

实例:8_8 exec函数实例

 1 #include"apue.h" 2 #include<sys/wait.h> 3 char *env_init[]={ "USER=unknow","PATH=/tmp",NULL}; 4 int main() 5 { 6     pid_t pid; 7     if((pid=fork())<0){ 8     err_sys("fork error"); 9     }else if(pid==0){//specify pathname,specify environment10     if(execle("/home/sar/bin/echoall","echoall","myarg1","MY ARG2",11         (char *)0,env_init)<0)12     err_sys("execle error");13     }14     if(waitpid(pid,NULL,0)<0)15     err_sys("wait error");16     if((pid=fork())<0){17     err_sys("fork error");18     }else if(pid==0){//specify filename,inherit environment19     if(execlp("echoall","echoall","only 1 arg",(char *)0)<0)20     err_sys("execlp error");21     }22     exit(0);23 }

 

8.11 更改用户ID和组ID

可以用setuid函数设置实际用户ID和有效用户ID。setgid函数设置实际组ID和有效组ID

#include<unistd.h>int getuid(uid_t uid);int setgid(gid_t gid);            //两个函数返回值:若成功则返回0,若出错则返回-1

规则:

(1):若进程具有超级用户权限,则setuid函数将实际用户ID、有效用户ID、以及保存的设置用户ID设置为uid

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

(3):如果上面两个条件都不满足,则将errno设置为EPERM,并返回-1

 

1.setreuid和setregid函数

交换实际用户ID和有效用户ID的值

#include<unistd.h>int setreuid(uid_t ruid,uid_t euid);int setregid(gid_t rgid,gid_t egid);            //两个函数返回值:若成功则返回0,若出错则返回-1

2.seteuid和setegid函数

只更改有效用户ID

#include<unistd.h>int seteuid(uid_t uid);int setegid(gid_t gid);        //返回值:T:0,F:-1

 

8.12 解释器文件

解释器文件是文本文件,其起始开头形式是:

#! pathname [optional-argument] 例如:#!/bin/sh

内核使调用exec函数的进程实际执行的不是解释器文件,而是该解释器文件第一行中pathname所指定的文件,一定要将解释器文件和解释器区分开来

8.13 system函数

#include<stdlib.h>int system(const char *cmdstring);

如果cmdstring是一个空指针时,system返回非零值,这特征可以确定在一个给定的操作系统上是否支持system函数

在UNIX中,system总是可用的

因为system在其实现中调用了fork、exec和waitpid,因此有三种返回值

(1)如果fork失败或者waitpid返回除EINTR之外的出错,则system返回-1,而且errno中设置了错误类型值

(2)如果exec失败,则其返回值如同shell执行了exit(127)一样

(3)否则所有三个函数都执行成功,并且system的返回值是shell的终止状态,其格式已在waitpid说明。

 

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

设置用户ID或设置组ID程序决不应调用system函数,因为system中执行了fork和exec之后超级用户权限仍会保持下来,如果一个进程正以特殊的权限运行,它又想生成另一个进程执行另一个程序,则它应当直接使用fork和exec,而且在fork之后,exec之前要改回到普通权限

 

8.14 进程会计

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

超级用户执行一个带路径名参数的accton命令启动会计处理。会计记录写到指定的文件中(会计记录结构定义在头文件<sys/acct.h>中)

会计记录所需的各种数据都由内核保存在进程表中,并在一个新进程被创建时置初值。每次进程终止时都会编写一条会计记录。这就意味着在会计文件中记录的顺序对应于终止的顺序,而不是他们启动的顺序

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

 

8.15 用户标识

系统通常记录用户登录时所使用的名字,用getlogin函数可以获取此登陆名

#include<unistd.h>char *getlogin(void);        //返回值:若成功则返回指向登陆名字符串的指针,若出错则返回NULL

如果调用此函数的进程没有连接到用户登录时所用的终端,则本函数会失败

 

8.16 进程时间

任意进程都可调用times函数以获得它自己及已终止子程序的:墙上时钟时间,用户cpu时间,系统cpu时间

#include<sys/times.h>clock_t times(struct tms *buf);  //返回值:若成功则返回流逝的墙上始终时间,若出错则返回-1