• 1.2 Linux中的进程 --- fork、vfork、exec函数族、进程退出方式、守护进程等分析


    fork和vfork分析:

      在fork还没有实现copy on write之前,Unix设计者很关心fork之后立即执行exec所造成的地址空间浪费,也就是拷贝进程地址空间时的效率问题,所以引入vfork系统调用。

      vfork有个限制,子进程必须立刻执行_exit或者exec函数。

      即使fork实现了copy on write,效率也没有vfork高,但是现在已经不推荐使用vfork了,因为几乎每一个vfork的实现,都或多或少存在一定的问题。

    fork:子进程拷贝父进程的数据段;vfork:子进程与父进程共享数据段。

    fork:父子进程的执行顺序不确定;vfork:子进程先运行,父进程后运行。

     vfork函数的目的就是创建一个子进程,然后把一个应用给加载起来,相当于用一个应用去替换这个子进程(替换代码段、数据段、堆栈段,修改进程控制块),vfork之后,如果子进程不立即拉起一个应用,而是执行其他操作,则很可能修改了和父进程共享的数据,造成不稳定现象。

      下面看一个vfork的例子:

     1 #include <sys/types.h>
     2 #include <unistd.h>
     3 
     4 #include <stdlib.h>
     5 #include <stdio.h>
     6 #include <string.h>
     7 
     8 #include <signal.h>
     9 #include <errno.h>
    10 
    11 #include <sys/stat.h>
    12 #include <fcntl.h>
    13 
    14 
    15 int main(void)
    16 {
    17     pid_t pid;
    18     int fd = 0;
    19     int abc = 10;
    20     
    21     printf("before fork pid : %d 
    ", getpid());
    22     
    23     pid = vfork();
    24     
    25     if(-1 == pid)
    26     {
    27         perror("pid < 0 ");
    28         return -1;
    29     }
    30     if(pid > 0)
    31     {
    32         printf("parent : pid : %d 
    ", getpid());
    33     }
    34     
    35     if(0 == pid)
    36     {
    37         printf("child : %d, parent : %d
    ", getpid(), getppid());
    38         printf("abc : %d
    ", abc);
    39     }
    40     
    41     printf("after ...
    ");
    42     return 0;
    43 }

    上面的程序中,vfork生成的子进程没有立即执行exit或者exec,而是做了两个打印操作,运行结果如下:

    我们在第38行访问了数据段中的abc变量,程序进入了死循环,产生了不稳定现象。我们在第38行程序的下一行加上一句exit(0),运行结果如下:

    这次运行就正常了。

      vfork主要用来拉起一个应用,我们创建一个文件hello.c,并写上如下程序:

    1 #include <stdio.h>
    2 
    3 int main()
    4 {
    5     printf("Hello World!
    ");
    6     return 0;
    7 }

      我们使用execve系统调用来拉起一个应用,修改vfork测试程序如下:

     1 #include <sys/types.h>
     2 #include <unistd.h>
     3 
     4 #include <stdlib.h>
     5 #include <stdio.h>
     6 #include <string.h>
     7 
     8 #include <signal.h>
     9 #include <errno.h>
    10 
    11 #include <sys/stat.h>
    12 #include <fcntl.h>
    13 
    14 
    15 int main(void)
    16 {
    17     pid_t pid;
    18     int fd = 0;
    19     int ret = 0;
    20     
    21     printf("before fork pid : %d 
    ", getpid());
    22     
    23     pid = vfork();
    24     
    25     if(-1 == pid)
    26     {
    27         perror("pid < 0 ");
    28         return -1;
    29     }
    30     if(pid > 0)
    31     {
    32         printf("parent : pid : %d 
    ", getpid());
    33     }
    34     
    35     if(0 == pid)
    36     {
    37         printf("child : %d, parent : %d
    ", getpid(), getppid());
    38         ret = execve("./hello", NULL, NULL);
    39         
    40         if(ret == -1)
    41         {
    42             perror("execve");
    43             exit(-1);
    44         }
    45         
    46         printf("execve execut failed
    ");
    47         
    48         exit(0);
    49     }
    50     
    51     printf("after ...
    ");
    52     return 0;
    53 }

      执行结果如下:

    由此可以看出,hello这个应用被成功拉起来了,子进程的整个进程空间被hello替换掉,因此后面的printf("execve execut failed ")便不会再执行。

      修改程序,拉起一个ls应用,如下所示:

     1 #include <sys/types.h>
     2 #include <unistd.h>
     3 
     4 #include <stdlib.h>
     5 #include <stdio.h>
     6 #include <string.h>
     7 
     8 #include <signal.h>
     9 #include <errno.h>
    10 
    11 #include <sys/stat.h>
    12 #include <fcntl.h>
    13 
    14 
    15 int main(void)
    16 {
    17     pid_t pid;
    18     int fd = 0;
    19     int ret = 0;
    20     char * const argv[] = {"ls", "-l", NULL};
    21     
    22     printf("before fork pid : %d 
    ", getpid());
    23     
    24     pid = vfork();
    25     
    26     if(-1 == pid)
    27     {
    28         perror("pid < 0 ");
    29         return -1;
    30     }
    31     if(pid > 0)
    32     {
    33         printf("parent : pid : %d 
    ", getpid());
    34     }
    35     
    36     if(0 == pid)
    37     {
    38         printf("child : %d, parent : %d
    ", getpid(), getppid());
    39         ret = execve("/bin/ls", argv, NULL);
    40         
    41         if(ret == -1)
    42         {
    43             perror("execve");
    44             exit(-1);
    45         }
    46         
    47         printf("execve execut failed
    ");
    48         
    49         exit(0);
    50     }
    51 
    52     return 0;
    53 }

    执行结果如下:

      从结果看出execve成功拉起了ls应用。

    进程终止的5种方式:

    进程终止有5种方式,分别为:

    正常退出:

      从main函数返回

      调用exit

      调用_exit

    异常退出:

      调用abort, 产生SIGABOUT信号

      由信号终止,ctrl+c  SIGINT

    其中exit和_exit的区别是:exit是c库函数,在退出之前会执行一些进程的清理工作,例如将用户空间缓冲区中的数据写到磁盘等,做完清理工作然后在调用_exit进入内核处理。_exit是系统调用,没有清理的过程,而是直接陷入内核去结束程序。二者的区别示意图如下:

    下面演示这两个函数的区别,首先调用的是exit,程序如下:

    1 #include <stdio.h>
    2 #include <unistd.h>
    3 #include <stdlib.h>
    4 
    5 int main()
    6 {
    7     printf("hello ... ");
    8     exit(0);
    9 }

    执行结果如下:

    将exit(0)替换为_exit(0)却什么都没有打印出来,现象分析:

      printf输出语句向终端写数据时是行缓冲的,也即遇到‘ ’时就会将数据从应用空间缓冲区写入内核,如果没有遇到换行符,就先将数据存在应用空间的缓冲区中,exit在退出时会先将应用空间缓冲区中的数据写入到内核,然后再去内核执行真正的退出,而_exit直接进入内核,而应用空间缓冲区中的数据就相当于不要了,所以直接调用_exit时没有任何打印。

      exit执行时还可以调用终止处理程序,这个程序时我们自己注册的,这个注册的api函数就是atexit,下面我们直接给出实验程序:

     1 #include <stdio.h>
     2 #include <unistd.h>
     3 #include <stdlib.h>
     4 
     5 void bye1()
     6 {
     7     printf("bye1 ... 
    ");
     8 }
     9 
    10 int main()
    11 {
    12     atexit(bye1);
    13     printf("hello ... 
    ");
    14     exit(0);
    15 }

    执行结果如下,终止处理程序被调用了:

    我们可以注册多个终止处理程序,而且先注册的后执行

      程序还可以调用abort异常退出,异常退出时,注册的终止处理程序不会被调用,演示程序如下:

     1 #include <stdio.h>
     2 #include <unistd.h>
     3 #include <stdlib.h>
     4 
     5 void bye1()
     6 {
     7     printf("bye1 ... 
    ");
     8 }
     9 
    10 int main()
    11 {
    12     atexit(bye1);
    13     printf("hello ... 
    ");
    14     abort();
    15     exit(0);
    16 }

    执行结果如下:

    最后一种进程终止方式就是向进程发信号,如果是一个杀死进程的信号,那么进程就会消失,其他信号可以将睡眠(可中断睡眠)进程唤醒。

      测试小程序如下:

     1 #include <stdio.h>
     2 #include <unistd.h>
     3 #include <stdlib.h>
     4 
     5 void bye1()
     6 {
     7     printf("bye1 ... 
    ");
     8 }
     9 
    10 int main()
    11 {
    12     atexit(bye1);
    13     printf("hello ... 
    ");
    14     sleep(100);
    15     printf("after ... 
    ");
    16     return 0;;
    17 }

      程序注册了终止处理程序,退出前睡眠100秒,在睡眠期间我们在键盘上按下crtl+c,执行结果如下:

    进程被终止,而且终止处理程序没有被调用。我们在键盘上按下的ctrl+c发出的是SIGINT信号,这个信号用来终止进程运行。

    SIGINT、SIGTERM、SIGKILL三者都是结束/终止进程运行,区别如下:

    1.SIGINT SIGTERM区别

    前者与字符ctrl+c关联,后者没有任何控制字符关联。

    前者只能结束前台进程,后者则不是。

    2.SIGTERM SIGKILL的区别

    前者可以被阻塞、处理和忽略,但是后者不可以。KILL命令的默认不带参数发送的信号就是SIGTERM.让程序有好的退出。因为它可以被阻塞,所以有的进程不能被结束时,用kill发送后者信号,即可。即:kill -9 进程号。

    exec函数族:

      在进程的创建上Unix采用了一种独特的方法,它将进程创建和加载一个新的进程映像相分离,这样做的好处是有更多的余地对两种操作进行管理。当我们创建了一个进程之后,通常将子进程替换成新的进程映像,这可以使用exec系列的函数来进行,当然exec系列的函数也可以将当前进程替换掉。

      exec函数族中的函数如下:

      int execl(const char *path, const char *arg, ...);
      int execlp(const char *file, const char *arg, ...);
      int execle(const char *path, const char *arg, ... , char * const envp[]);


      int execv(const char *path, char *const argv[]);
      int execvp(const char *file, char *const argv[]);

    它们的关系如下:

    只有execve是系统调用,其他几个只是库函数,是对execve的封装,前三个函数中的函数名字中 l 代表可变参数列表,p代表在PATH环境变量中搜索file文件,e代表环境变量。后面两个函数中v代表需要传入指针数组argv。 以上函数中,带p的函数只需要传入文件名,不带p的函数需要传入路径名。

    下面演示execlp的使用,程序如下:

     1 #include <sys/types.h>
     2 #include <unistd.h>
     3 
     4 #include <stdlib.h>
     5 #include <stdio.h>
     6 #include <string.h>
     7 
     8 #include <signal.h>
     9 #include <errno.h>
    10 
    11 #include <sys/stat.h>
    12 #include <fcntl.h>
    13 
    14 
    15 int main(void)
    16 {
    17     printf("before execlp 
    ");
    18     execlp("ls", "ls", "-l", NULL);
    19     
    20     printf("after execlp 
    ");
    21     return 0;
    22 }

    执行结果如下:

    execlp是对execve系统调用的封装,简化了函数的使用,l代表是可变参数,p代表PATH环境变量,我们只需要给这个函数传入可执行文件名,系统会自动根据PATH变量的值搜索这个文件。

      我们使用execlp拉起一个自己写的应用,如下:

    1 #include <stdio.h>
    2 
    3 int main()
    4 {
    5     printf("app getpid() : %d
    ", getpid());
    6     return 0;
    7 }

    修改主控制函数:

     1 #include <sys/types.h>
     2 #include <unistd.h>
     3 
     4 #include <stdlib.h>
     5 #include <stdio.h>
     6 #include <string.h>
     7 
     8 #include <signal.h>
     9 #include <errno.h>
    10 
    11 #include <sys/stat.h>
    12 #include <fcntl.h>
    13 
    14 
    15 int main(void)
    16 {
    17     printf("getpid() : %d 
    ", getpid());
    18     execlp("./execlp-getpid", NULL, NULL);
    19     
    20     return 0;
    21 }

    执行结果如下:

    可见,原来的进程在拉起应用之后,进程pid是不变的。

      接着对execle进行实验分析,下面演示一个环境变量相关的小程序,这个小程序是被主控制程序拉起来的应用,程序如下所示:

     1 #include <sys/types.h>
     2 #include <unistd.h>
     3 #include <stdio.h>
     4 #include <errno.h>
     5 
     6 extern char **environ;
     7 
     8 int main(void)
     9 {
    10     int i = 0;
    11     printf("before printf environ ... 
    ");
    12     
    13     for(i = 0; environ[i] != NULL; i++)
    14     {
    15         printf("%s
    ", environ[i]);
    16     }
    17     
    18     return 0;
    19 }

    这个小程序如果单独执行的话,它会打印系统中所有的环境变量,如下所示:

    下面我们给出主控制程序,这个程序将上面的打印环境变量的应用拉起来,最主要的函数是execle,具体如下:

     1 #include <sys/types.h>
     2 #include <unistd.h>
     3 #include <stdio.h>
     4 
     5 #include <errno.h>
     6 
     7 
     8 int main(void)
     9 {
    10     printf("getpid() : %d 
    ", getpid());
    11     execle("./environ", NULL, NULL);
    12     printf("after execle... 
    ");
    13     return 0;
    14 }

    execle中传入环境变量的部分我们给的是NULL指针,执行结果如下:

    可见,被拉起来的应用中的for循环没有得到执行,这跟我们传入的NULL指针是有关系的。

      如果我们想在程序中定义自己的环境变量,并传给即将拉起来的应用程序,该怎么实现呢?修改主控制程序如下,打印环境变量的程序保持不变。

     1 #include <sys/types.h>
     2 #include <unistd.h>
     3 #include <stdio.h>
     4 
     5 #include <errno.h>
     6 
     7 
     8 int main(void)
     9 {
    10     char * const argv[] = {"aaa=111", "bbb=222", NULL};
    11     printf("getpid() : %d 
    ", getpid());
    12     
    13     execle("./environ", NULL, argv);
    14     printf("after execle... 
    ");
    15     return 0;
    16 }

    执行结果如下,打印出了我们自己定义的环境变量:

    守护进程:

      守护进程是在后台运行不受终端控制的进程,通常情况下守护进程在系统启动时自动运行。

      守护进程的名称通常以d结尾,比如sshd、xinetd、crond等。

    创建守护进程的步骤如下:

    1、调用fork创建新进程,它会是将来的守护进程

    2、在父进程中调用exit,保证子进程不是进程组组长

    3、调用setsid创建新的会话期

    4、将当前目录改为根目录(如果把当前目录作为守护进程的目录,当前目录不能被卸载,它作为守护进程的工作目录了)

    5、标准输入、标准输出、标准错误重定向到/dev/null

     下面分析一个客户端登录框架,如下图:

    telnet客户端登录到服务器上,会进行用户名和密码的校验,校验成功后,也就登录完成了,服务器会创建一个会话期,然后在这个会话期中默认执行一个shell。然后这个shell会去用户目录下执行$HOME/.bash_profile文件,这个shell是为这个用户服务的。

      这个登录相当于在客户端和服务器之间建立了一个会话期(session),在这个会话期里面可以有很多进程组,默认执行的shell就成为这个会话期中的一个进程组,当我们在这个shell上执行ps -ef | grep wbm01时,ps进程和grep进程成为一个进程组,它们和shell不属于一个进程组,但都在同一个会话期中。进程组组长的pid就是进程组的组号。现在执行的shell、ps、grep或者我们自己的hello程序都是和终端有关联的,所以它们都不是守护进程。

      如果我们想要做一个后台服务程序即守护进程,那么我们必须从这个会话期中跳出来,单独创建一个会话期,在新会话期中有我们自己fork出来的进程myforkproc,这个进程就可以脱离中断的控制了,这就是守护进程。创建守护进程的过程可以按以上我们给出的步骤来进行,也可以使用daemon一步完成。创建一个新会话的时候不能是进程组组长来调用setsid,所以应该先fork一个子进程,让子进程来调用setsid。调用setsid的进程将成为新会话期的leader进程,会话期id就是这个进程的pid,这个进程也会是新会话期中一个进程组的组长。

      跳出已有会话期,创建新会话期的框图如下:

    演示程序如下:

     1 #include <sys/types.h>
     2 #include <unistd.h>
     3 
     4 #include <stdlib.h>
     5 #include <stdio.h>
     6 #include <string.h>
     7 
     8 #include <signal.h>
     9 #include <errno.h>
    10 
    11 int main()
    12 {
    13     pid_t pid;
    14     
    15     pid = fork();
    16     
    17     if(-1 == pid)
    18     {
    19         perror("fork error");
    20         exit(-1);
    21     }
    22     
    23     if(pid > 0)
    24     {
    25         exit(0);
    26     }
    27     
    28     pid = setsid();
    29     
    30     if(-1 == pid)
    31     {
    32         perror("setsid error");
    33         exit(0);
    34     }
    35     
    36     sleep(100);
    37     
    38     printf("after deamon ...
    ");
    39     return 0;
    40 }

    执行程序,结果如下:

    可以看到a.out进程对应的终端那一列显示的是“?”,问号就代表这个进程没有终端,就是后台守护进程。

      根据创建守护进程的步骤,我们上面的程序还缺少两步,下面给出一个完整的程序:

     1 #include <sys/types.h>
     2 #include <unistd.h>
     3 
     4 #include <stdlib.h>
     5 #include <stdio.h>
     6 #include <string.h>
     7 
     8 #include <signal.h>
     9 #include <errno.h>
    10 
    11 #include <sys/stat.h>
    12 #include <fcntl.h>
    13 
    14 
    15 int main()
    16 {
    17     pid_t pid;
    18     
    19     pid = fork();
    20     
    21     if(-1 == pid)
    22     {
    23         perror("fork error");
    24         exit(-1);
    25     }
    26     
    27     if(pid > 0)
    28     {
    29         exit(0);
    30     }
    31     
    32     pid = setsid();
    33     
    34     if(-1 == pid)
    35     {
    36         perror("setsid error");
    37         exit(0);
    38     }
    39     
    40     chdir("/");
    41     int i = 0;
    42     for(i = 0; i < 3; i++)
    43     {
    44         close(i);
    45     } 
    46     
    47     open("/dev/null", O_RDWR);
    48     dup(0);
    49     dup(0);
    50     
    51     sleep(100);
    52     
    53     printf("after deamon ...
    ");
    54     return 0;
    55 }

      新添加的第40行将守护进程的工作目录设置为根目录,守护进程的工作目录默认为启动这个程序的目录,如果这个目录有被卸载的可能,则因为守护进程对这个目录的占用而不能卸载,所以要将工作目录设置为根目录。

      工作目录设置完成,然后关闭标准输入、标准输出、标准错误,这时候0,1,2三个文件描述符就空闲了,打开/dev/null,这个文件就占用了0描述符,dup函数负责将0号文件描述符复制到文件描述符表中的空闲项中,本例中也就是1和2。

      下面我们演示调用daemon来创建守护进程,程序如下:

     1 #include <sys/types.h>
     2 #include <unistd.h>
     3 #include <stdlib.h>
     4 #include <stdio.h>
     5 #include <errno.h>
     6 
     7 int main()
     8 {
     9     daemon(0, 0);
    10     
    11     printf("after ...
    ");
    12     return 0;
    13 }

      第一个参数0表示改变工作目录,第二个参数0表示关闭标准输入、标准输出、标准错误,第二个参数为0时,没有任何打印,因为标准输出关闭了,重定向到了/dev/null,如果第二个参数不为零,执行结果如下:

    最后一句话打印出来了,说明守护进程没有关闭标准输出。

  • 相关阅读:
    ie 火狐兼容集锦
    ie css png
    jQuery插件——autoTextarea-文本框根据输入内容自适应高度
    比onload更快获取图片尺寸(转载)
    数据库性能问题排查
    项目管理_FindBugs的使用
    js动态获取子复选项并设计全选及提交
    SVN使用_获取某版本后改动的文件列表
    存储过程_把字符串转化为结果集
    Spring下如何配置bean
  • 原文地址:https://www.cnblogs.com/wanmeishenghuo/p/9348170.html
Copyright © 2020-2023  润新知