8.1 引言算法
本章介绍UNIX的进程控制,包括建立新进程、执行程序和进程终止。还将说明进程属性的各类ID-----实际、有效和保存的用户和组ID,以及他们如何受到进程控制原语的影响。本章还包括了解释器文件和system函数。本章最后讲述大多数UNIX系统所提供的进程会计机制。这种机制使咱们可以从另外一个角度了解进程的控制功能。shell
8.2 进程标识符数组
每一个进程都有一个非负整型表示的唯一进程ID。由于进程标识符是唯一的,常将其用做其余标识符的一部分以保证其唯一性。虽然是唯一的,可是进程ID能够重用。(大多数UNIX系统实现延迟重用算法,使得赋予新建进程的ID不一样于最近终止进程所使用的ID。这防止了将新进程误认为是使用同一ID的某个已终止的进程。编辑器
ID为0一般是系统进程函数
ID为1一般是init进程优化
除了进程ID,每一个进程还有其余一些标识符。下列函数返回这些标识符ui
#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
这些函数都没有出错返回spa
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