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

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
相关文章
相关标签/搜索