zoukankan      html  css  js  c++  java
  • 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
    ";
     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
    ");//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
    ",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
    ");
    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
    ",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
    ",WEXITSTATUS(status));
     7     else if(WIFSIGNALED(status))
     8     printf("abnormal termination,signal number= %d%s
    ",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
    ",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
    ",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
    ");
    11     }else {
    12     charatatime("output from parent
    ");
    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
  • 相关阅读:
    atitit.TokenService v3 qb1 token服务模块的设计 新特性.docx
    Atitit attilax在自然语言处理领域的成果
    Atitit 图像清晰度 模糊度 检测 识别 评价算法 原理
    Atitit (Sketch Filter)素描滤镜的实现  图像处理  attilax总结
    atitit。企业的价值观 员工第一 vs 客户第一.docx
    Atitit 实现java的linq 以及与stream api的比较
    Atitit dsl exer v3 qb3 新特性
    Atititi tesseract使用总结
    Atitit 修改密码的功能流程设计 attilax总结
    atitit.TokenService v3 qb1  token服务模块的设计 新特性.docx
  • 原文地址:https://www.cnblogs.com/wbingeek/p/3852451.html
Copyright © 2011-2022 走看看