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
  • 相关阅读:
    蝶恋花
    JVM解毒——JVM与Java体系结构
    超赞!IDEA 最新版本,支持免打扰和轻量模式!
    SpringBoot 结合 Spring Cache 操作 Redis 实现数据缓存
    神奇的 SQL 之 WHERE 条件的提取与应用
    终于放弃了单调的swagger-ui了,选择了这款神器—knife4j
    Git 高级用法,喜欢就拿去用
    既然有 HTTP 请求,为什么还要用 RPC 调用?
    SpringBoot和Spring到底有没有本质的不同?
    一条简单的更新语句,MySQL是如何加锁的?
  • 原文地址:https://www.cnblogs.com/wbingeek/p/3852451.html
Copyright © 2011-2022 走看看