当前位置: 首页 > news >正文

Linux|fork命令及其使用的写时拷贝技术

fork复制进程

fork通过以下步骤来复制进程:

  1. 分配新的进程控制块:内核为新进程分配一个新的进程控制块(PCB),用于存储进程的相关信息,如进程 ID、状态、寄存器值、内存指针等。
  2. 复制进程地址空间:将父进程的地址空间(包括代码段、数据段、堆和栈等)复制到新进程的地址空间中。这意味着新进程将拥有与父进程相同的程序代码和数据。
  3. 复制文件描述符表:父进程打开的文件描述符在子进程中也会被复制,使得子进程可以访问相同的文件资源。
  4. 设置进程状态和 ID:新进程被设置为就绪状态,等待被调度执行。同时,内核为子进程分配一个唯一的进程 ID。
  5. 返回控制:fork系统调用在父进程和子进程中都返回。在父进程中,返回值是子进程的进程 ID;在子进程中,返回值是 0。通过检查返回值,父进程和子进程可以区分彼此,并执行不同的代码路径。

简单来说:fork是把已有的进程复制一份,把对应父进程的PCB也复制了一份,然后申请一个PID,子进程,子进程的PID=父进程的PID+1;

通过这些步骤,fork创建了一个与父进程几乎完全相同的子进程,子进程可以独立于父进程运行,并可以在适当的时候执行自己的代码逻辑。

在Linux上示例:

创建一个main.c,通过if  else返回值来操作父子进程做不一样的事情:

#include <stdio.h>

#include <unistd.h>

#include <assert.h>

#include <stdlib.h>


int main()

{

    char *s=NULL;

    int n=0;//控制父子进程执行的次数;



    pid_t id=fork();

    assert(id!=-1);



    if(id==0)//子进程

    {

        s="child";

        n=3;

    }

    else//父进程

    {

        s="parent";

        n=7;

    }

    //父子进程

    int i=0;

    for(;i<n;i++)

    {

        printf("s=%s\n",s);

        sleep(1);

    }

    exit(0);

}

多运行几次就会发现每次执行结果不完全一定:

要解释这执行结果不同的原因,我们先了解下printf的缓冲区机制

下面介绍一下printf的缓冲区机制

printf函数通常会将输出先存储在缓冲区中,而不是立即输出到终端或其他目标设备。这样做的目的是为了提高输出效率,减少系统调用的次数。例如,当多次调用printf输出少量数据时,这些数据会先在缓冲区中积累,直到缓冲区满或者遇到特定的条件(如换行符\n),才会将缓冲区中的内容一次性输出。

在Linux上用代码演示一下:

创建一个main.c文件

使用exit(0)命令退出程序

其中,exit是先刷新缓冲区,然后再调用_exit(真正的退出); _exit直接退出,不会刷新缓冲区;

编译后运行,结果是三秒后输出hello(注意这里hello后没有加\n,\n会刷新缓冲区,后面会详细总结)

原因是程序执行printf时先将hello放到缓冲区,不是直接打印到屏幕上,然后执行sleep(3)休眠3秒,然后执行到exit(0)时先刷新缓冲区,此时屏幕上显示hello,随后程序结束运行。

强制刷新 (1)方法一:遇到\n自动刷新 printf("hello\n"); (2)使用fflush刷新屏幕 fflush(stdout);

总结: printf将内容先写入到缓冲区中,缓冲区刷新到界面(屏幕)上的条件是:

(1)缓冲区放满

(2)缓冲区未满,强制刷新缓冲区到屏幕(方法一:\n;方法二:主动刷新:fflush(stdout));

(3)程序结束时,自动刷新缓冲区:exit方法

了解printf缓冲区这一特点后便清楚fork实际的运行结果不确定的原因了:

当使用fork创建子进程时,子进程会继承父进程的内存空间,包括printf函数的缓冲区。如果在父进程中printf了一些内容但缓冲区尚未刷新,那么在fork之后,子进程中也会有一份相同的未刷新缓冲区内容。这可能导致一些意外的输出结果。例如,如果父进程在fork之前调用printf输出了一些字符串但没有换行,然后fork创建了子进程,接着父进程和子进程都继续执行,那么可能会出现父进程和子进程的输出混合在一起的情况,因为它们共享了原来的缓冲区内容,并且在后续的执行中可能会各自刷新缓冲区。

fork的时机:

fork产生的这个子进程不是从头开始执行的,而是从fork之后开始执行的,就是说子进程直接从fork下面的代码开始执行,具体的是说从得到fork的返回值后子进程开始执行,子进程不会再fork了,所以不会出现子进程再去fork产生一个子进程的问题. 也就是说:fork返回值语句,父进程fork返回子进程的PID,子进程fork返回0,二者分别独立执行各自的程序。

补充系统调用函数:getppid和getpid(头文件<unistd.h>

getppid:得到一个进程的父进程的PID;

getpid:得到当前进程的PID;

我们在原先的代码中增加这两个函数调用:

#include <stdio.h>

#include <unistd.h>

#include <assert.h>

#include <stdlib.h>


int main()

{

    char *s=NULL;

    int n=0;//控制父子进程执行的次数;



    pid_t id=fork();

    assert(id!=-1);



    if(id==0)//子进程

    {

        s="child";

        n=3;

    }

    else//父进程

    {

        s="parent";

        n=7;

    }

    //父子进程

    int i=0;

    for(;i<n;i++)

    {

        printf("s=%s,pid=%d,ppid=%d\n",s,getpid(),getppid());

        sleep(1);

    }

    exit(0);

}

运行结果如下图:

观察发现父进程的父进程pid对应bash的pid,这里的bash是系统命令解释器:

此处拓展介绍一下命令解释器:

在计算机科学中,命令解释器Shell俗称壳(用来区别于核),是指“为使用者提供操作界面”的软件(command interpreter,命令解析器)。它类似于DOS下的COMMAND.COM和后来的cmd.exe。它接收用户命令,然后调用相应的应用程序。

我们就是通过命令解释器(称为shell)(bash是命令解释器中的一种)和内核和系统进行交互的(Windows通过图形界面进行交互的);例如我们把ls交给bash,bash帮我们运行ls,然后把结果给用户;

 回归正题——fork

fork背后多采用写时拷贝技术

写时拷贝技术

写时拷贝是一种延迟拷贝的策略。在fork创建子进程时,并不立即复制父进程的所有内存页面,而是让父进程和子进程共享这些页面,并将这些页面标记为只读。只有当父进程或子进程试图对某个共享页面进行写操作时,操作系统才会为该页面分配新的内存空间,并将原页面的内容复制到新的页面中,然后让进行写操作的进程在新的页面上进行修改。

 如果不使用写时拷贝技术

第一:复制开销比较大; 第二:占用内存空间;

使用写时拷贝技术:

父子进程逻辑地址一样,但是物理地址是不一样的(多进程逻辑地址相同,对应的物理地址不一定相同)

简单来说就是fork的时候,子进程直接把父进程的页表复制过来,子进程发生写入(修改)的时候才分配内存复制,然后进行相应的页表修改,因此写时拷贝是一种可以推迟甚至免除拷贝数据的技术。

内容来源内核与设计22页:


到此fork及其背后运用的写时拷贝技术就介绍完啦!

如果觉得有用可以点个赞,谢谢支持呀,会持续输出更新知识点,感兴趣可以关注一下!

相关文章:

  • HyperAD:学习弱监督音视频暴力检测在双曲空间中的方法
  • id: ‘dev.flutter.flutter-plugin-loader‘, version: ‘1.0.0‘怎么解决
  • The Illustrated Stable Diffusion
  • [贪心算法]买卖股票的最佳时机 买卖股票的最佳时机Ⅱ K次取反后最大化的数组和 按身高排序 优势洗牌(田忌赛马)
  • 基础篇结束纪念——Java抽象类 模板类 static接口
  • 基于springboot的校园周边美食探索及分享平台(021)
  • 基于生成对抗网络(GAN)的图像超分辨率重建:技术与应用
  • 【深度】JADC2的层级结构以及全域Mesh网络
  • K8S集群新增和删除Node节点(K8s Cluster Adds and Removes Node Nodes)
  • 【动态规划】矩阵连乘问题 C++(附代码实例和复杂度分析)
  • 3. 轴指令(omron 机器自动化控制器)——>MC_SetOverride
  • react 常用插件
  • axios 请求的底层依赖是什么?
  • 系统思考—啤酒游戏经营决策沙盘模拟
  • 牛客网编程题调试问题(华为研发工程师编程题JavaScript Node调试)
  • C++ - 从零实现Json-Rpc框架-1(JsonCpp Muduo 异步操作)
  • 【Linux】统信操作系统进入单用户如何修改密码
  • SQL 通配符
  • 大白话详细解读React框架的diffing算法
  • Python+selenium,轻松搭建Web自动化测试框架
  • 企业网站的web应用环境通常有几种组合/mac923水蜜桃923色号
  • 旅游网站的网页设计素材/网络推广和网站推广
  • 做网站编辑要有逻辑吗/农产品品牌推广方案
  • 海阔天空网站建设/如何制作网页广告
  • 浪琴女士手表网站/制作网站的最大公司
  • 网站开发的目的/风云榜