进程编程中的fork起步

本文详细介绍了进程编程中的fork函数,探讨了父子进程之间的关系及其区别,并通过示例代码展示了如何利用fork创建子进程,同时深入分析了fork循环、父子进程间的资源共享及写时复制机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

进程编程中的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开始),也可叫页式管理机制。只复制对应的页。缺页,在中断查询,再赋值。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值