【Linux】——轻松搞定进程fork

目录

​编辑

前言

创建子进程

fork进程

分离子进程

写时拷贝

结语


前言

  在现代计算机系统中,多任务处理已经成为一种常态。从服务器上同时处理多个用户请求,到我们的个人电脑上各种软件后台默默运行的小任务,这一切的背后都离不开进程的管理。而在创建新进程的技术手段中,fork函数可谓是老牌且经典的“选手”。无论是在类UNIX系统的开发,还是在深入了解计算机系统运行机制的学习过程中,理解fork都是非常重要的。无论你是一个想要深入学习操作系统底层原理的开发者,还是仅仅对计算机内部运行机制充满好奇的探索者,这篇关于fork的博文都将为你打开一扇深入了解的大门。

创建子进程

在上一篇博文中,我们初步认识了子进程和父进程的关系,也知道了子进程是由父进程创建的

每一个子进程都会有一个父进程,这是必须的

那么,父进程如何创建子进程呢?

在Linux下,创建进程有两种方式

  1. 在Linux命令行中使用指令创建
  2. 程序自己创建

我们说过,在Linux中一般都是父进程创建子进程

在Linux命令行中执行命令,就是bash创建进程去完成对应的指令!

而在程序中,我们可以调用C程序中的系统接口 fork函数 来创建进程

fork进程

命令行中创建进程朴实无华,无法进行优雅的操作,我们不过多赘述

今天的主要介绍对象—— fork进程 

fork函数是一个在类 Unix 操作系统(如 Linux、macOS 等)中用于创建新进程的系统调用函数。新创建的进程被称为子进程(child process),而调用 fork 的进程则称为父进程(parent process)。

函数造型:

pid_t  fork(void);

返回值

  • 成功:在父进程中,fork 返回子进程的进程ID(PID),这是一个正整数;在子进程中,fork 返回 0
  • 失败:返回 -1,并设置全局变量 errno 来指示错误原因。

基本用法:

pid_t pid = frok();

简单的一行代码,便是创建了一个子进程

当子进程被创建后,它会和父进程一起执行 frok() 之后的代码,相当于执行了两次代码

父进程一次,子进程一次

验证一下:fork()后的代码会被执行两次

#include<iostream>
#include<unistd.h>
#include<sys/acct.h>

int main()
{
    std::cout<<"你好,现在是frok之前的代码"<<std::endl;
    pid_t id = fork();
    std::cout<<"你好,现在是frok之后的代码"<<std::endl;
    return 0;
}

 一段朴实无华的代码,代码的运行结果,应该是

  1. 你好,现在是frok之前的代码
  2. 你好,现在是frok之后的代码

 运行一下,结果

结果并非我们所愿,而后面的打印语句执行了两次,前面的打印语句执行了一次

证明了 frok() 切切实实地创建了一个子进程,一个新的执行流

新创建的子进程只会执行frok之后的代码,不会执行之前的代码

分离子进程

但我们创建子进程的目的,不是为了重复执行父进程的操作,而是为了让多进程协同完成任务或者父进程执行这个任务的时候,子进程去完成其他任务

我们创建子进程的目的——让子进程去完成我们期望的任务

如何让自子进程脱离去执行别的任务?

回看frok函数的返回值介绍,你会发现它有两个返回值,很双标

  1. 对父进程,它会返回 子进程的pid
  2. 对子进程,它会返回一个 整数 0

如此,我们可以用不同的返回值,在后续的代码中分离父子进程,让二者各司其职

实践一下:利用不同返回值分离父子进程

    pid_t pid = fork();
    if(pid < 0){
        std::cout<<"子进程创建失败,可以回家了"<<std::endl;
        return -1;
    }else if(pid == 0){
        std::cout<<"你好,我是子进程,我的pid是:"<<getpid()<<std::endl;
    }else{
        std::cout<<"你好,我是父进程,我创建的子进程pid是"<<pid<<std::endl;
    }
    return 0;

这是一段经典的用if语句进行父子进程分离的代码,建议以后就都这么干

运行结果: 

符合我们的预期和理解,没有问题,perfect!

虽然解决了父子进程重复执行代码的问题,但相信大家仍有疑问

  1. 为什么子进程会执行父进程的代码?
  2. 子进程和父进程共享代码,为什么只能执行frok之后的代码?
  3. 为什么frok函数会有两个返回值?
  4. frok之后,谁先执行?
  5. frok函数的返回值为什么不一样

一一解答大家的疑惑

为什么子进程会执行父进程的代码?

子进程是一个全新的进程,当它被父进程创建之后,操作系统在内存中也会为它创建一个PCB,这个PCB是依照父进程的PCB创建的,其中的大部分属性和父进程PCB相同。

属性中有两个指针,分别指向可执行程序在内存中的代码和指针,父子进程PCB中的指针一样,就会找到内存中的同一片区域,执行相同的代码,用一样的数据

子进程和父进程共享代码,但为什么子进程只能执行frok之后的代码

子进程的PCB依照父进程创建,其中大部分属性继承自父进程

每个进程执行或者暂停的时候,调入和调出,会形成自己的上下文,这些上下文中会记录自己的代码暂停到了哪里,产生了哪些数据。

子进程PCB继承父进程PCB属性时,也会将上下文一起继承,当它开始执行的时候,父进程执行到哪里,它就从哪里开始执行!

为什么frok函数会有两个返回值

frok函数的功能是创建一个新进程

我们通常认为,frok函数执行完后创建了一个新进程,而真实情况是,在frok运行的时候,就已经创建了新进程

frok函数是一个有返回值的函数,当它执行到return语句的时候,函数才算执行完毕,而return语句仅仅只是一个返回数据的语句,没有其他任何职能

所以,在frok函数执行 return 语句前,就已经创建了新进程,并将其投入调度队列

简单看一下 frok 函数内部

父进程开始执行frok函数

  1. 操作系统找到父进程的PCB
  2. 依照父进程的PCB创建子进程的PCB
  3. 操作系统给子进程PCB指针赋值,让子进程PCB的指针指向父进程的代码和数据
  4. 让子进程准备好,将其PCB放入调度队列,等待CPU

等等,后面省略

也就是说,执行到return语句前,操作系统就已经创建子进程,并将其放入调度队列

此时,可能就已经有两个执行流,在执行frok函数,也就是说,frok函数会被执行两次,返回两个值也就不奇怪了

frok函数的return语句被执行了两次,就会返回两个值

frok之后,父子进程谁先执行?

不清楚,但唯一可以确定的是,父子进程是PCB都会进入调度队列,等待CPU的调度,具体谁先被执行,无法确定。

这是由进程的优先级,操作系统的调度算法等共同决定的,父进程不会因为自己更大一级而被优待,子进程也不会因为自己更小一级而被礼让。

写时拷贝

子进程共享父进程的代码和数据

代码是只读的,不会应发问题

而数据是父子进程各自拥有一份

子进程直接拷贝一份父进程的数据给自己吗?

并不是,如果完全拷贝一份数据给自己,是不划算的,因为子进程不会对父进程的所有数据进行修改,可能只会修改其中的一两个数据。

为了避免拷贝全部数据带来的不必要损耗,操作系统采用写时拷贝的策略为子进程提供独立的数据。

写时拷贝

  • 当子进程修改了父进程的数据,才会自己拷贝一份,成为自己独立的数据,其他没有修改的数据,仍是直接使用父进程的数据即可

结语

  在这趟探索进程fork的奇妙旅程中,我们一同揭开了它在操作系统世界里的神秘面纱。从最初对其作用的懵懂好奇,到逐渐深入理解它如何在系统中创建出一个个充满活力的子进程,就像在微观的数字宇宙中发现了一颗独特的星辰,它在进程的浩瀚星空中闪烁着智慧的光芒。

  希望这次的探索能成为你深入操作系统领域的基石,让你在未来的编程之旅中,如同拥有一把神奇的钥匙,轻松地驾驭进程管理的奥秘。愿你在数字世界的广袤海洋中,继续畅游,发现更多令人惊叹的技术宝藏。