进程编程中的fork起步

来源:互联网 发布:linux必备软件 编辑:程序博客网 时间:2024/06/14 22:15

进程编程中的fork起步

父子进程的关系

  使用fork(或者其他能创建子进程的API)函数得到的子进程从父进程的继承了整个进程的地址空间,包括:进程上下文、进程堆栈、代码段、数据段、以及PCB和内存信息、打开的文件描述符、信号控制设置、进程优先级、进程组号、当前工作目录、根目录、资源限制、控制终端等。
  
  子进程与父进程的区别在于
  (1)父进程设置的锁,子进程不继承
  (2)PID和父进程PID不同
  (3)子进程的未决警告被清除
  (4)子进程的未决信号集设置为空集。

fork函数

  • 头文件:<sys/types.h> 和 <unistd.h>
  • 函数功能:创建一个子进程
  • 函数原型:pid_t fork(void);
  • 返回值:
      (1)成功创建一个子进程,对于父进程来说返回子进程ID
      (2)成功创建一个子进程,对于子进程来说返回值为0
      (3)如果为-1表示创建失败
     这里写图片描述
  • 示例代码
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <sys/types.h>#include <errno.h>#include <signal.h>int main(){  pid_t pid;//记录子进程的PID  int tmp = 250;//测试子进程拷贝了父进程的堆栈段(临时变量存在于栈空间)                  //--但是拷贝以后,各自操作自己的堆栈,互不影响                //--意味着父子进程各有一个tmp  signal(SIGCHLD,SIG_IGN);//用于处理僵尸进程                            //--操作系统会通知父进程子进程结束了                                                    //--使用该函数会让父进程忽略该通知  printf("before fork...pid: %d\n",getpid());//打印fork之前的进程PID--即父进程PID    pid = fork();//创建子进程  if(pid == -1)//创建失败  {    perror("fork");    return -1;    }  else if(pid > 0)//父进程从内核空间返回到用户空间--捕获到一个大于零的数--子进程的PID  {    tmp++;//在父进程的地址空间处理局部变量    printf("this is parent pid: %d\ttmp = %d\n",getpid(),tmp);  //打印父进程的信息  }  else if(0 == pid)//子进程从内核空间的就绪队列被调度开始执行--从内核空间返回到用户空间  {    tmp++;//在子进程的地址空间处理局部变量--这里的tmp是基于fork之前从父进程栈空间拷贝的--原始值位250,已经与父进程的tmp无关    printf("this is child pid: %d\tparent : %d\ttmp = %d\n",getpid(),getppid(),tmp); //打印子进程的信息   }  printf("after fork...pid: %d\n",getpid());//fork之后的代码会被拷贝到子进程,所以这条语句会被父子进程都执行                                              //--根据pid可以判断是父进程还是子进程执行的以及它们的执行顺序    return 0;}

几个需要理解的问题

  1. fork系统调用之后,父子进程将交替执行。顺序是随机的!

  2. 一次调用2次返回?
    问题的本质是:两次返回,是在各自的进程空间中返回的。子进程和父进程各有自己的内存空间/进程空间 (fork:代码段、数据段、堆栈段、PCB进程控制块的copy)。

  3. fork返回值大于零的是父进程,为什么这样设计?
    父进程可能产生多个子进程,为了更好的管理控制子进程,父进程需要知道自己要控制哪一个子进程,用子进程的PID来给父进程识别子进程是最好的方法,所以返回一个大于零的数,作为子进程的PID,是为了方便父进程对子进程的管理。

  4. 怎么样理解分支在fork之后,而不是父进程main函数的开始?
    子进程在创建以后,就复制了父进程的堆栈段,而在fork之前,面入口后的这部分代码是对堆栈的处理和初始化!父进程已经做好了初始化的工作,子进程的堆栈要和父进程完全一样,所以子进程不必再进行重复的初始化工作!

难点分析

  • fork循环
    父进程中执行了n次fork将会产生2的n次方个进程(包括最开始的父进程),fork后面的代码将会呗每个进程都执行!这种情况下,父生子,子生孙,不好控制!有点像二叉树的关系!
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <sys/types.h>#include <errno.h>#include <signal.h>int main(){  int i = 0;  for(i = 0;i < 3;i++)//父进程中执行了n次fork将会产生2的n次方个进程(包括最开始的父进程),                        //fork后面的代码将会呗每个进程都执行  {    fork();  }  printf("Aloha!Fork!\n");  return 0;}
  • 一个父亲生出多个儿子但不允许生出孙子
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <sys/types.h>#include <errno.h>#include <signal.h>void Test(int var){  printf("Aloha!Fork()....%d\n",var);}int main(){  int loop_num = 0;  int proc_num = 0;  int i = 0,j = 0;  pid_t pid ;  signal(SIGCHLD,SIG_IGN);  printf("Input the procnum:\n");  scanf("%d",&proc_num);  printf("Input the loopnum:\n");  scanf("%d",&loop_num);  for(i= 0; i < proc_num;i++)  {    pid = fork();    if(0 == pid)    {      printf("child : %d\tparent : %d\n",getpid(),getppid());//父进程如果退出以后,会将创建的子进程托孤给init进程--1号进程--父进程的PID就是1      for(j = 0;j < loop_num;j++)      {        Test(j);      }      exit(0);//一旦子进程完成任务立即结束子进程--避免子进程执行下一次循环的fork--避免子生孙    }  }  return 0;}
  • 一个父亲只生出一个儿子,世代单传
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <sys/types.h>#include <errno.h>#include <signal.h>void Test(int var){  printf("Aloha!Fork()....%d\n",var);}int main(){  int loop_num = 0;  int proc_num = 0;  int i = 0,j = 0;  pid_t pid ;  printf("Input the procnum:\n");  scanf("%d",&proc_num);  printf("Input the loopnum:\n");  scanf("%d",&loop_num);  for(i= 0; i < proc_num;i++)  {    pid = fork();    if(0 == pid)    {      printf("child : %d\tparent : %d\n",getpid(),getppid());//父进程如果退出以后,会将创建的子进程托孤给init进程--1号进程--父进程的PID就是1      for(j = 0;j < loop_num;j++)      {        Test(j);      }    }        else if(pid > 0)    {      exit(0);//一旦父进程创建完子进程,从内核空间返回用户空间就结束父进程--避免父进程执行下一次循环的fork--避免子多余的儿子--保证单传    }else if(pid == -1)    {      perror("fork");      return -1;    }  }  return 0;}
  • 父子进程共享文件
    实际上,在创建子进程以后,文件描述符属于堆栈空间或者数据段内容,确实是从父进程复制到了子进程,也就是说确实存在两份文件描述符,而且互不相关!只不过两个文件描述符都指向磁盘上的同一份文件,所以看起来是共享文件描述符!类似于两个指针指向同一片内存空间!正是由于文件描述符是互不相关的两个变量,所以在父子进程需要分别关闭文件!不然有一个文件描述符还指向磁盘上的文件!这不安全!
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <sys/types.h>#include <errno.h>#include <signal.h>#include <fcntl.h>#include <sys/stat.h>int main(){  pid_t pid;//记录子进程的PID  int fd ;  signal(SIGCHLD,SIG_IGN);//用于处理僵尸进程                            //--操作系统会通知父进程子进程结束了                                                    //--使用该函数会让父进程忽略该通知  fd = open("test.txt",O_CREAT|O_RDWR,0755);//在创建子进程之前先打开文件--具备一个文件描述符  if(fd == -1)  {    perror("Open");    return -1;  }  printf("before fork...pid: %d\n",getpid());//打印fork之前的进程PID--即父进程PID    pid = fork();//创建子进程  if(pid == -1)//创建失败  {    perror("fork");    return -1;    }  else if(pid > 0)//父进程从内核空间返回到用户空间--捕获到一个大于零的数--子进程的PID  {    write(fd,"parent",6);//父进程向已打开的文件写入内容    printf("this is parent pid: %d\n",getpid());  //打印父进程的信息    close(fd);  }  else if(0 == pid)//子进程从内核空间的就绪队列被调度开始执行--从内核空间返回到用户空间  {    write(fd,"child",5);//子进程向已打开的文件写入内容--如果该内容追加到父进程的内容之后--说明父子进程共享文件    printf("this is child pid: %d\tparent : %d\n",getpid(),getppid()); //打印子进程的信息        close(fd);   }  printf("after fork...pid: %d\n",getpid());//fork之后的代码会被拷贝到子进程,所以这条语句会被父子进程都执行                                              //--根据pid可以判断是父进程还是子进程执行的以及它们的执行顺序    return 0;}

写时复制简介

  • 如果多个进程要读取它们自己的那部分资源的副本,那么复制是不必要的。–只读取就不进行复制!加快子进程创建速度!
  • 每个进程只要保存一个指向这个资源的指针就可以了。–子进程如果不修改堆栈段和数据段内容,只需要有一个指针指向父进程相应的资源即可!并不是执行完毕fork之后立即复制数据段和堆栈段等资源!
  • 如果一个进程要修改自己的那份资源的“副本”,那么就会复制那份资源。这就是写时复制的含义–子进程执行写操作就会拷贝资源,为了避免影响其他进程对自己资源的管理和访问嘛!

原因分析:加快速度,linux内核是段页式管理机制(因段管理从0开始),也可叫页式管理机制。只复制对应的页。缺页,在中断查询,再赋值。

0 0