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); //返回值:调用进程的进程id pid_t getppid(void); //返回值:调用父进程的进程ID uid_t getuid(void); //返回值:调用进程的实际用户id uid_t geteuid(void): //返回值:调用进程的有效用户id gid_t getid(void) //返回值:调用进程的实际组id gid_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 stdout 14 if((pid=fork())<0){ 15 err_sys("fork error"); 16 }else if(pid==0){ //child 17 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 #else 12 ""); 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 environment 10 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 environment 19 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