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. int main(void)
  15. {
  16. pid_t pid;
  17. int fd = ;
  18. int abc = ;
  19.  
  20. printf("before fork pid : %d \n", getpid());
  21.  
  22. pid = vfork();
  23.  
  24. if(- == pid)
  25. {
  26. perror("pid < 0 ");
  27. return -;
  28. }
  29. if(pid > )
  30. {
  31. printf("parent : pid : %d \n", getpid());
  32. }
  33.  
  34. if( == pid)
  35. {
  36. printf("child : %d, parent : %d\n", getpid(), getppid());
  37. printf("abc : %d\n", abc);
  38. }
  39.  
  40. printf("after ...\n");
  41. return ;
  42. }

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

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

这次运行就正常了。

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

  1. #include <stdio.h>
  2.  
  3. int main()
  4. {
  5. printf("Hello World!\n");
  6. return ;
  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. int main(void)
  15. {
  16. pid_t pid;
  17. int fd = ;
  18. int ret = ;
  19.  
  20. printf("before fork pid : %d \n", getpid());
  21.  
  22. pid = vfork();
  23.  
  24. if(- == pid)
  25. {
  26. perror("pid < 0 ");
  27. return -;
  28. }
  29. if(pid > )
  30. {
  31. printf("parent : pid : %d \n", getpid());
  32. }
  33.  
  34. if( == pid)
  35. {
  36. printf("child : %d, parent : %d\n", getpid(), getppid());
  37. ret = execve("./hello", NULL, NULL);
  38.  
  39. if(ret == -)
  40. {
  41. perror("execve");
  42. exit(-);
  43. }
  44.  
  45. printf("execve execut failed\n");
  46.  
  47. exit();
  48. }
  49.  
  50. printf("after ...\n");
  51. return ;
  52. }

  执行结果如下:

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

  修改程序,拉起一个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. int main(void)
  15. {
  16. pid_t pid;
  17. int fd = ;
  18. int ret = ;
  19. char * const argv[] = {"ls", "-l", NULL};
  20.  
  21. printf("before fork pid : %d \n", getpid());
  22.  
  23. pid = vfork();
  24.  
  25. if(- == pid)
  26. {
  27. perror("pid < 0 ");
  28. return -;
  29. }
  30. if(pid > )
  31. {
  32. printf("parent : pid : %d \n", getpid());
  33. }
  34.  
  35. if( == pid)
  36. {
  37. printf("child : %d, parent : %d\n", getpid(), getppid());
  38. ret = execve("/bin/ls", argv, NULL);
  39.  
  40. if(ret == -)
  41. {
  42. perror("execve");
  43. exit(-);
  44. }
  45.  
  46. printf("execve execut failed\n");
  47.  
  48. exit();
  49. }
  50.  
  51. return ;
  52. }

执行结果如下:

  从结果看出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();
  9. }

执行结果如下:

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

  printf输出语句向终端写数据时是行缓冲的,也即遇到‘\n’时就会将数据从应用空间缓冲区写入内核,如果没有遇到换行符,就先将数据存在应用空间的缓冲区中,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 ... \n");
  8. }
  9.  
  10. int main()
  11. {
  12. atexit(bye1);
  13. printf("hello ... \n");
  14. exit();
  15. }

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

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

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

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

执行结果如下:

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

  测试小程序如下:

  1. #include <stdio.h>
  2. #include <unistd.h>
  3. #include <stdlib.h>
  4.  
  5. void bye1()
  6. {
  7. printf("bye1 ... \n");
  8. }
  9.  
  10. int main()
  11. {
  12. atexit(bye1);
  13. printf("hello ... \n");
  14. sleep();
  15. printf("after ... \n");
  16. return ;;
  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. int main(void)
  15. {
  16. printf("before execlp \n");
  17. execlp("ls", "ls", "-l", NULL);
  18.  
  19. printf("after execlp \n");
  20. return ;
  21. }

执行结果如下:

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

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

  1. #include <stdio.h>
  2.  
  3. int main()
  4. {
  5. printf("app getpid() : %d\n", getpid());
  6. return ;
  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. int main(void)
  15. {
  16. printf("getpid() : %d \n", getpid());
  17. execlp("./execlp-getpid", NULL, NULL);
  18.  
  19. return ;
  20. }

执行结果如下:

可见,原来的进程在拉起应用之后,进程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 = ;
  11. printf("before printf environ ... \n");
  12.  
  13. for(i = ; environ[i] != NULL; i++)
  14. {
  15. printf("%s\n", environ[i]);
  16. }
  17.  
  18. return ;
  19. }

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

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

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

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

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

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

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

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

守护进程:

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

  守护进程的名称通常以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(- == pid)
  18. {
  19. perror("fork error");
  20. exit(-);
  21. }
  22.  
  23. if(pid > )
  24. {
  25. exit();
  26. }
  27.  
  28. pid = setsid();
  29.  
  30. if(- == pid)
  31. {
  32. perror("setsid error");
  33. exit();
  34. }
  35.  
  36. sleep();
  37.  
  38. printf("after deamon ...\n");
  39. return ;
  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. int main()
  15. {
  16. pid_t pid;
  17.  
  18. pid = fork();
  19.  
  20. if(- == pid)
  21. {
  22. perror("fork error");
  23. exit(-);
  24. }
  25.  
  26. if(pid > )
  27. {
  28. exit();
  29. }
  30.  
  31. pid = setsid();
  32.  
  33. if(- == pid)
  34. {
  35. perror("setsid error");
  36. exit();
  37. }
  38.  
  39. chdir("/");
  40. int i = ;
  41. for(i = ; i < ; i++)
  42. {
  43. close(i);
  44. }
  45.  
  46. open("/dev/null", O_RDWR);
  47. dup();
  48. dup();
  49.  
  50. sleep();
  51.  
  52. printf("after deamon ...\n");
  53. return ;
  54. }

  新添加的第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(, );
  10.  
  11. printf("after ...\n");
  12. return ;
  13. }

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

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

1.2 Linux中的进程 --- fork、vfork、exec函数族、进程退出方式、守护进程等分析的更多相关文章

  1. Linux系统编程——进程替换:exec 函数族

    在 Windows 平台下,我们能够通过双击运行可运行程序,让这个可运行程序成为一个进程.而在 Linux 平台.我们能够通过 ./ 运行,让一个可运行程序成为一个进程. 可是.假设我们本来就执行着一 ...

  2. Linux进程实践(3) --进程终止与exec函数族

    进程的几种终止方式 (1)正常退出 从main函数返回[return] 调用exit 调用_exit/_Exit (2)异常退出 调用abort   产生SIGABOUT信号 由信号终止  Ctrl+ ...

  3. 20_Android中apk安装器,通过WebView来load进一个页面,Android通知,程序退出自动杀死进程,通过输入包名的方式杀死进程

     场景:实现安装一个apk应用程序的过程.界面如下: 编写如下应用,应用结构如下: <RelativeLayout   编写activity_main.xml布局: <Relative ...

  4. UNIX环境编程学习笔记(20)——进程管理之exec 函数族

    lienhua342014-10-07 在文档“进程控制三部曲”中,我们提到 fork 函数创建子进程之后,通常都会调用 exec 函数来执行一个新程序.调用 exec 函数之后,该进程就将执行的程序 ...

  5. 2.3 进程控制之exec函数族

    学习目标:学习使用exec函数族的重要的几个函数  一.引言 进程通过exec函数根据指定的文件名或目录名执行另一个可执行文件,当进程调用exec函数时,该进程的数据段.代码段和堆栈段完全被新程序替换 ...

  6. 在linux中安装jdk以及tomcat并shell脚本关闭启动的进程

    在命令行模式中输入uname -a ,如下图,当界面展示i386就说明本linux系统为32版本,就在官网下载对应jdk版本,或者直接到我的网盘上下载http://pan.baidu.com/s/1c ...

  7. 对linux中source,fork,exec的理解以及case的 使用

    fork   使用 fork 方式运行 script 时, 就是让 shell(parent process) 产生一个 child process 去执行该 script, 当 child proc ...

  8. APUE8进程控制 fork vfork exec

  9. Linux中crontab下scp文件传输的两种方式

    Linux下文件传输一般有两个命令scp.ftp(工具需要下载安装) 本文主要讲讲scp的文件传输脚本 1.scp ssh-keygen -t rsa免输入密码,传输 这里假设主机A 用来获到主机B的 ...

随机推荐

  1. spoj TBATTLE 质因数分解+二分

    题目链接:点击传送 TBATTLE - Thor vs Frost Giants #number-theory #sliding-window-1 Thor is caught up in a fie ...

  2. python 集合清空

    setp = set(["Red", "Green"]) setq = setp.copy() print(setq) setp.clear() print(s ...

  3. MAC下Java安装之后的路径

    pwd /Library/Java/JavaVirtualMachines/jdk1.8.0_171.jdk/Contents/Home 安装好jdk之后,就开始配置环境变量了. 首先,在终端输入 s ...

  4. CASSANDRA How to import and export data

    https://docs.datastax.com/en/cql/3.1/cql/cql_reference/copy_r.html 感谢领导,感谢同事,与其自己百思不得其解,不如一个问题就搞定了. ...

  5. 关于python中的 “ FileNotFoundError: [Errno 2] No such file or directory: '……'问题 ”

    今天在学python时,在模仿一个为图片加上图标并移动到指定文件夹的程序时遇到“FileNotFoundError: [Errno 2] No such file or directory: '152 ...

  6. 数据库使用SSIS进行数据清洗教程

    OLTP系统的后端关系数据库用于存储不同种类的数据,理论上来讲,数据库中每一列的值都有其所代表的特定含义,数据也应该在存入数据库之前进行规范化处理,比如说“age”列,用于存储人的年龄,设置的数据类型 ...

  7. RabbitMQ入门_12_发布方确认

    参考资料:https://www.rabbitmq.com/confirms.html 通过 ack 机制,我们可以确保队列中的消息一定能被消费到.那我们有办法保证消息发布方一定把消息发送到队列了吗? ...

  8. C#使用xpath查找xml节点信息

    Xpath是功能很强大的,但是也是相对比较复杂的一门技术,最好还是到博客园上面去专门找一些专业的帖子来看一看,下面是一些简单的Xpath语法和一个实例,提供给你参考一下. xml示例: <?xm ...

  9. php file_get_contents计时读取一个文件/页面 防止读取不到内容

    php file_get_contents计时读取一个文件/页面 防止读取不到内容 $url = 'http://www.baidu.com/index.php'; $opts = array( 'h ...

  10. 开发shellcode的艺术

    专业术语 ShellCode:实际是一段代码(也可以是填充数据) exploit:攻击通过ShellCode等方法攻击漏洞 栈帧移位与jmp esp 一般情况下,ESP寄存器中的地址总是指向系统栈且不 ...