进程 概念和理解 - Linux 是怎么做到 管理进程的?-fork 手动创建进程

前言

上一篇博客当中,对 冯诺依曼体系结构 和 操作系统 进行了简要概述,本篇博客将会从上一篇博客的基础之上进行展开,如果你有些不了解的话,建议先看上一篇博客再看本篇博客:

冯诺依曼体结构 - 为什么要有操作系统-CSDN博客

进程概念

 就算你不知道 操作系统,一大概率听说过进程,其实进程就是 已经加载到内存当中程序正在运行的程序) ,进程是一定要被加载到内存的 ,这些程序就是又程序员编写的 一个一个软件。其实 一个进程本质上也可以称之为是一个 任务,这样获取你会有更好的理解。

在windows 当中,按下 ctrl + shift + esc 就可以的打开任务管理器,或者邮件任务栏,也可以打开,当我们打开 任务管理器之后,就可以的看到当前你的计算机正在执行哪一些进程:

 在 linux 当中可以使用 以下命令来查看当前 那些进程正在被执行:
 

ps axj

其中的 PID 就是这个进程专属的 进程编号。 

PPID (parent process ID)就是 当前进程的 父进程 PID

像上述的 ps ajx | head -1 && ps ajx | grep myprocess 这个指令当中,使用了 "&&" 这个符号,这个符号表示要左右两边的指令都运行成功才能输出结果,同样的,在同一位置我们可以使用 ";" 分号来代替他,这个 ";" 分号的意思就是 左右两边的指令都要执行。

我们发现上述多过滤出一个 进程 :

这个其实是 grep 自己的进程,以为 grep 要过滤就要先把自己给 调入内存,自己运行起来,然后才能查找。因为 上述命令当中 grep myprocess 本身就到了 myprocess 这个关键词,所以就把自己给查找出来了。 

其中还有一个 COMMAND 这个属性,代表的是这个进程当前是 使用了哪一个命令调用的进程:

 或者使用 Top 命令来查看当前正在运行的进程信息(同时,你还可以实时的看到当前运行所有进程,有多少个 进程处于什么 进程状态):

 进程的理解

 首先,进程再被加载到 内存之前,是一个程序,那么一个程序,就又代码文件和一些附带文件所存储,既然是文件,那么一个程序在被加载之前,就是在磁盘上存储的;

也就是说,进程再被加载到 内存之前,实在磁盘上进行存储的,所以,对于 冯诺依曼体系结构当中的 输入设备,对于进程来说,就是磁盘了。

 此时,在 磁盘当中的 程序要想被cpu执行,就要先加载到 内存当中,而 程序文件,本质上也就是存储一些二进制的文件,由 代码 和 数据构成的程序二进制文件。都是二进制的数据,只是有些二进制数据 表示代码,有些表示数据。

那么 ,是代码的二进制数据就交给 控制器去执行;是 数据的二进制数据就交给 运算器去执行。

归根结底还是 把 磁盘的当中的程序的二进制数据 读取到了内存当中。

那么问题来了,虽然cpu 当中一次只能执行一个任务,但是,一个操作系统不可能值运行一个进程,可以同时运行多个进程。比如:我既可以使用 QQ音乐听歌,又可以使用 word 写文档;还可以打开浏览器 看 B站。

但是,在上述说的不同的 进程当中,我可能先写 word 文档,在看 B站,当B站的当前视频在写完 word 之前就播放完了,此时我想就不看 B站视频了,那么 浏览器就被我关掉结束了,但是此时我还是再写 word 文档。

也就是说,各个进程之间是什么时候开始的,什么时候执行结束的,我们不能完全去控制,他们在同一时间片当中,有各自进程的执行状态。那么,在上述情况下,操作系统必须管理好各个进程如何让操作系统合理的运行?就是需要解决的问题。

 如何管理进程?

 在上一篇博客对操作系统的描述当中就说到了,要想让计算机帮我们处理数据,就得前把数据描述起来(用 struct 或者 class 把各个对象的属性包装起来)。

那么接下来的工作就是,管理好由各个进程 包装成的一个一个对象,那么无非就是 把各个进程增删查改,就得用 某种数据结构来对这些个对象,进行某种逻辑上的链接。

 任何一个进程,在加载到内存时,在由 程序形成 真正 的进程之前,要对 这个程序当中 属性 管理这个进程需要的 属性 包装到一个 结构体(或者类,因为操作系统是由C实现的,所以这里统称为结构体)当中,用于描述这个进程。我们把这个结构体对象称之为 -- PCB(process ctrl block)-  进程控制块

 其实计算机来辨别新事物 的方式 和 人是一样的,任何在辨别新事物的时候,其实很难的看到这个事物的本身,我们认识到 这个事物 ,知道这个动物叫做 老虎,是它表现出来的特征,让我们认为他是一个老虎;人是通过 属性 来认识事物的计算机也是一样

 当某个事物表现的属性够多时,这些属性的集合(对象)就是目标对象。

 在描述进程也是一样的,当描述一个进程的属性足够多的时候,那么就可以认为,这是一个进程(可以理解为 进程 就是属性的集合)。这本质上就是一种 面相对象

 在用C实现的操作系统当中,一个属性的集合 其实就是一个 struct(结构体)


 那么,进程之前的属性千奇百怪,操作系统可以创建很多个 结构体,操作系统如何分辨哪一个是哪一个进程的结构体呢?

其实很简单,在学校当中,假设是新生,老师不知道名字,长相等各个属性的情况下,可以如何分辨这些学生呢?

就是学号,新生的名字可能是一个生僻字,老师不会读,各个新生可能有自己的爱好和特长,老师一时半会也分辨不出来。但是,数字老师总得认识吧,那么他只需要从 学校教务处拿到一个新生名单,就可以分按照学号来分辨新生了。

同样,计算机可能不会认识哪一个变量代表的是什么意思,但是,如果给每一个进程都编上属于这个进程唯一的编号,那么计算机总得是可以分辨数字的,所以就可以分辨出进程。

 除了进程编号之外,还会有 优先级,进程的状态等等 共同属性,多少可以帮助 操作系统识别进程的。


总结:

当一个程序被加载到内存当中时,操作系统首先要干的事情就是,根据这个 程序的PCB类型,为这个进程开辟一个 PCB对象,对这个对象当中的各个属性进行初始化。

在上述的过程当中,操作系统可以选择先不加载 程序当中的 数据 和 代码的二进制数据到内存当中,可以先为 这个进程构造一个 PCB 对象。

但是,程序的当中的二进制数据总是要加载到 内存当中的,那一个PCB 当中只是关于这个对象的属性的存储,但是 进程的 二进制数据还是要开空间来存储:就好比是:你养了一只狗,你用笔记录了 这只狗的 属性特征,这个的存储大小是可能是一张纸,或者是一个笔记本;但是,你要想在家当中养活这只狗,那么得为这个狗买狗窝,给他空间作家。


什么是进程(理解 管理进程的过程)

所以,一个 内核PCB数据结构对象 不叫进程;一个 code & data (文件二进制数据(代码和数据)) 也不叫进程,这两个 加起来 才叫一个进程。

操作系统会把 一个 PCB  和 一个 code & data(文件二进制数据) 构建成一个 结构体对象,这个结构体对象是由操作系统自己生成的。

 所以,操作系统要管理这个进程,根本就不看 这个进程的 代码和数据,只看 PCB对象当中的属性就行了。

 我可以使用数据结构把 各个进程当中的 PCB 对戏那个按照这个数据结构的链接方式进行链接,此时,我们要想对这个进程进行管理,就只需要对这个 存储 各个 PCB 对象数据结构的增删查改进行 操作即可。

比如,在我们 PCB对象当中增加一个 PCB* next 指针,指向下一个 PCB对象,那么现在各个PCB 就是一个 单链表了,此时,在操作系统当中,对进程进行管理,变成了对 这个 PCB对象的单链表进行增删查改的操作了。


所以,我们可能会看到某一个进程正在排队等待运行,不是这个进程的 二进制文件数据 在排序,而是这个 进程的 PCB对象正在排队

就像在公司当中投简历排队一样,面试官是按字符串查找它需要的什么技能的人,然后去查找简历当中符合要求的人,此时,如果你等不及了,打电话问公司人力资源部咨询人员,他会告诉你,你正在排队,或者你已经被录取,或者你被淘汰。而不是 公司的 leader 一个一个人打电话去问,这就太挫了。

如果你此时正在排队,是你的 投进去的简历在公司当中的队列当中排队,而不是你这个人在公司当中排队。


总结:一个进程包括 属性的和数据,数据是这个进程运行自己的算法逻辑,和一个需要的数据;属性是 操作系统管理这个 进程。什么时候等待,什么时候运行,什么时候结束(死亡)等等操作所需的 PCB对象当中的属性。

操作系统管理 进程是他通过属性进行管理,而不是通过进程当中的 数据。

Linux 是怎么做到 管理进程的?

各个操作系统之间虽然都是按照 先描述在组织的方式来实现的,但是,对于操作系统的实现细节还是很大差异的。

比如:各个操作系统之间的 关于 PCB 的数据结构的实现就不同。

描述 Linux进程的 - PCB

进程信息 被放在一个叫做 进程控制块数据结构 中,可以理解为 进程属性的集合

课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct

这个 task_struct 是一个大型的结构体,这里面包含了Linux 内核当中 描述进程 所以需要用到的属性。 

 首先应该理解的是,task_struct 是 PCB 的一种,属于 PCB。在windows ,macOS 当中都有自己各有的 PCB。

  • Linux描述进程的结构体叫做 task_struct
  • task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息
     

而在Linux 当中 链接 各个 PCB对象(注意:只是PCB对象,而不是 二进制数据 和 PCB对象),最基本组织进程 tash_struct 的方式  ,一般是使用 双向链表来 链接的。

而且,在,对于一个 进程 PCB 对象,不仅仅会放在的一个 双链表当中,还可能放在一个 队列当中。其实想做到这个并不难,在 C语言当中,只需要多创建一个 数据结构的 PCB* 指针就行。

比如有等待队列,运行队列等等数据结构,我们想把某一个进程 当前状态进行修改,就只需要把这个 进程 的PCB 对象放到 某一个 在组织的数据结构 当中就行,至于操作系统怎么运行,是按照这个数据结构当中的算法来执行的。

数据结构背后是配套的算法,而算法背后又是具体的应用场景。

 task_struct 的内容分类

  •  标示符: 描述本进程的唯一标示符,用来区别其他进程。
  • 状态: 任务状态,退出代码,退出信号等。(比如,当前进程执行的状态怎样的,是再等待运行代码?还是正在运行代码?代码运行后它的状态码是什么?进程是新建的,正在运行的,正在休眠的,死亡的等等状态)
  • 优先级: 相对于其他进程的优先级。(操作系统当中是有很多个进程在同时运行的,那么进程和进程之间就存在这竞争关系)
  • 程序计数器: 程序中即将被执行的下一条指令的地址。(程序一般是 在栈帧当中顺序执行的,但是,程序一般情况下不是从上往下顺序跑完的,可能还调用函数,循环等等往返着调用语句,这时,我们如何在在调用完函数,知道下一个应该执行完哪一个语句,就需要 PC指针,或者程序计数器来记录跳转执行指令下一个语句的指针。这个程序计数器是 cpu 的一个寄存器,这个寄存器就用来存储下一个语句的地址)
  • 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
  • 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
  • I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
  • 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。(比如:某某个进程累计在cpu 当中运行了多少时间)
  • 其他信息

在 Linux 当中有一个 目录(/proc):(process 的简写),当中就放了 现在执行的 动态运行的 所以进程 信息:

 我们发现,此时,由很多蓝色的数字目录,正在运行:

而且,这些目录是以数字命名的,这个数字其实就是 某一个进程的 PID。所以,我们就理解了,如果某一个进程开始运行了,那么 操作系统就会在 /proc 这个目录当中,以这个进程的 PID 创建一个 目录,这个目录当中保存了这个进程的大部分属性

当这个进程结束执行结束之后,在 /proc 目录当中创建的 以 PID 为名字的文件夹就会被自动销毁。 

我们现在可以查看某一个进程当中的 各个属性的文件:
 

 其中有一个 exe 文件夹,这个文件当中指向的就是 这个可执行文件的 绝对目录地址。

还有一个 cwd,当前进程的工作目录。比如:我们使用 C当中的 fopen()函数,自动帮我们创建一个文件的时候,是在当前目录下创建的,而这个 cwd 所指的目录就是这里所指的当前目录。

比如现在使用 touch 命令创建文件的时候,意思就是在当前目录下创建文件,你说,这不是废话吗?但是,touch指令是写的一个软件实现的,那么touch 指令要运行,就要在提取到内存当中进行运行,这时就需要 cwd了。所以,cwd的意思就是:当前进程是在哪一个目录下运行的,那么 cwd 就指向哪一个目录。


我们可以直接使用  kill -9 进程编号 命令 ,来直接干掉一个进程,这个命令的意思就是给 这个进程发送 9 号信号:

kill -9 xxxxx

 有时,进程可能会 奔溃,卡死,等等情况,我们可以使用 ctrl + c 来结束,但是有时候 ctrl + C 都不能结束,就可以使用 上述指令来强制结束一个进程。


在上篇博客对 为什么要有操作系统当中我们而已提到了,操作系统在链接 各个 进程的过程中,本质是对 各个进程的 PCB对象进行 链接,而链接方式可能是各种的数据结构算法,比如:在Linux 当中就是以 双链表的方式来连接各个 PCB 对象的。

那么 像我们在开始使用的 类似 ps axu 指令,查看 当前运行的各个进程的指令。本质上就是用 C/C++ 写的 遍历在操作系统当中 存储链接各个 PCB 的数据结构

 所以,如果现在用户想要访问到当前某个进程的 PID 进程编号的话,不期望直接使用 C 中 访问结构体的方式来访问到 某一个结构体对象当中的 成员属性。而是像C++ 当中访问类的私有成员一样,给出一个接口来访问。

所以,我们在系统上想访问到 某一个 进程的信息,就要使用 类似 ps 这样的指令,也就是要用要用 系统调用接口 的方式来调用。


 我们可以用 一个简单的 命令来实时监控某一个进程(就是用命令当中的一个 一直执行循环来执行 )

# 下述 grep 当中的 -v 选项是反向选择
while :; do ps axj | head -1 && ps axj | grep text | grep -v grep; echo "--------------------------------" sleep 3; done

此时他就会实时监控 text 这个程序是否成为一个进程,像上述,因为text可执行程序没有执行,所以,上述什么都没有检测到,当我们运行了 text 之后,就可以检测到了,我们在 text 可执行程序当中,使用 getpid()这个函数来获取到 本程序生成的 进程的 PID,这个函数的返回值是 pid_t 是有符号整形:
 

 当 text 进程运行时,就会一直打印 本进程的 PID:
 

此时我们采用上述的检测 进程的 命令再次执行:

先执行的 监视命令。然后在执行 text 进程,发现原本没有的 进程信息就出现了。 


而且,我们发现,同一个 程序,每一次运行操作系统自动生成的 PID 可能是不一样的,但是这是相当正常的。

我们需要注意的是:PID 只在当前进程 允许的过程当中是有效的,如果是这个进程被重新执行了,或者是已经结束执行了,这个原本这个进程的 PID 就不在适用了。

如上所示,两次执行的text程序生成的 PID 都是不一样的 。 


同样的,你还可以使用 getppid()这个函数来获取到 当前程序变成进程 系统生成的 PPID:

 执行结果:

 但是发现,不管怎么运行text这个程序,这个 由 text 生成的进程  的  PPID 都是不变的:
 

 此时我们就查看一下 此时这个 28785 到底是什么?

ps axj | head -1 && ps axj | grep 28785 | grep -v grep

打印:
 

 我们发现 只有一个 PID 为 28785 的进程。

相信你已经猜到了,这个进程就是 bash -- 命令行解释器,这个解释器就可以 解释我们输入的命令从而指令对应命令的软件。

我们在 Linux 当中所有执行的指令都是 由经过命令行解释器的,所有的  命令都是 bash 的子进程。所以,当其中的某一个子进程奔溃了,或者是报错 了,执行错误,都不会影响到 bash 这个进程。只会影响到 子进程。

fork 手动创建进程

 我们之前都是使用类似于 "./" 的方式,直接调用某一个 可执行程序,这样的话,操作系统就会自动帮我们创建一个 管理这个进程的 PCB文件,然后再在 /proc 这个目录之下创建一个 和这个 进程的编号(PID)相同的 文件夹目录, 这个目录当中就存放了 关于这个 进程的属性。

但是上述都是操作系统 自己帮助我们创建的,如果想要手动创建一个进程的话,可以使用 fork

这个是一个函数,我们先使用 man forc 使用手册来查看关于 fork 的介绍
 

fork 是一个函数用于 创建一个子进程。 

此时我们在 Linux 当中编写一个 小程序来验证:
 

在这个进程当中, "end:~!" 在最后只打印了一遍,但是输出却输出了两遍:
 

 在前面两个打印的 begin: this is a process! 和 第一个end:~! 是原本第一个进程执行的结果,其实在 fork()函数之后,后面的代码语句已经被新生成了一个进程,所以,当我们运行 text 这个可执行文件的时候,相当于是运行了 两个进程,后一个进程就是 printf("end:~!\n"); 这个语句

forc()函数的作用就是在 调用的进程为模版,新创建一个 进程。这个新进程就是调用 fork()函数的这个进程 的 子进程。

  fork()函数的返回值是 pid_t 类型:

  • 如果 fork()函数创建进程成功了,那么将会给父进程返回一个当前新产生的子进程的 PID,同时返回 0 给子进程。
  • 如果 fork()函数创建进程失败了,返回 -1 给父进程,此时没有 子进程被创建,给出error 错误码。

相信你已经发现了,当 fork()函数创建进程成功之后,我们看上去返回了两个返回值,一个给 父进程,一个给子进程。

要探究上面的问题,我们下来看这个例子:我们直接在 fork()函数之后,写一个 if 判断一个 fork()函数的返回值。那么此时,如果fork()返回的是 0 ,说明当前执行的是父进程,如果返回的是一个 >0 的数,那么说明当前进行的是 子进程,如果返回的是 <0 的数,返回的是一个 error:
 

 如上所示,为了方便观察,在三种情况之下都 sleep()一下。

输出:
 

 首先,在进程卡开始执行之后执行的是子进程,然执行的是父进程,而且,子进程父进程 之间是交替执行的。但是我们写的 不是 子进程 父进程 分开的两个死循环吗?怎么会 两个交替执行呢?怎么会出现 两个死循环同时再跑呢?

 而且,上述的输出结果是不是就证明了,我们用 Mypid 这个变量来接收的 fork()函数的返回值,等于0 和 大于0 两个条件同时成立了

 这就是因为我们使用了 fork()函数,在 fork()函数之后,变成了两个进程,一个是原本的进程,我们此时称之为 父进程;另一个是 fork()函数新 生成的 子进程。这两个是实实在在的父子关系:

此时你就要注意区分了:

  • 使用 "./" 方式是在指令层面 帮我们创建进程。
  • 使用 fork()函数是在 代码层面 帮我们创建进程。


 我们现在来梳理一下 关于此时 text 这个程序执行的顺序

刚开始执行 text 进程的时候,操作系统就给这个 text 分配了一个 PID 为  18375,此时这个进程也就是 父进程。

在 执行 fork()函数之后,就有 fork()函数一分为二,形成两个分支,一个是之前的进程 --- 父进程;另一个就是 --- 子进程。

之前我们单独 执行 text 进程,就是一个单进程的当时执行的,但是,因为这个 text 当中有一个 fork()函数,这个函数帮助我们创建一个子进程,此时就是多进程的执行方式了。

为什么fork()函数要给 子进程返回 0 ,给父进程返回 子进程 的 PID?

我们先来看,为什么 给 子进程 和 父进程 的返回值要不同?

  •  首先,返回不同的返回值,是为了区分,让不同的执行流,执行不同的代码块。
  • 其次,fork()之后的代码父子共享。像之前给的死循环那个例子,其实 fork()之后的代码,父进程 和 子进程 都是一样的,只不过我们在上述以 fork()函数返回值进行 区分而已。

上述只是解决了 为什么返回值要不同,但是为什么 给子进程返回的是 0 ,而 给父进程 返回的是 子进程的 PID?

 在现实当中,一个父亲可能以后多个孩子,但是一个孩子只能有一个亲生父亲;父进程 可能要对 子进程做一些控制。所以,父进程要拿到 子进程的 PID,用来管理子进程,标识子进程的唯一性;子进程不一样,他只有一个父亲,他只需要找到 PPID 就可以找到父进程

fork()函数干了什么事?

 在 fork()函数 创建新 子进程之前,在系统当中就已经 创建了原本的进程 --- 父进程了,之前说过,进程的本质实际上就是 PCB(内核数据结构) + 代码和数据

在调用 fork()函数之后,系统当中创建一个子进程,创建一个子进程本质上不就是 在系统当中多一个进程吗?

所以,此时在  父进程的PCB 创建的基础之上,由多了一个 子进程 PCB;子进程当中的各个属性其实是按照父进程 为 模版来创建的;

但是此时有一个问题,父进程有 自己的 PCB 和 代码数据,但是子进程是系统创建的,在创建之前没有 像 父进程一样从 磁盘当中获取到 代码和数据,所以,子进程只能无奈的 和父进程 一样,调用同样的代码。

 所以,此处需要注意的是,fork()之后,父子进程访问的代码是共享的。在 C/C++ 当中,代码被加载到内存之后,代码是不能被修改的,能改的只能是代码当中的数据,

为什么要创建子进程呢?如果 子进程和 父进程 执行的是一样的内容,那么直接让 父进程去干不就可以了,为什么要  子进程呢?所以,我们就是要让 父 和子进程 执行不同的事情!像办法让 子进程 和 父进程 执行不同的代码块。这样,让父子做不同的事情,让 父子协同起来。

所以 fork()函数才有了不同的返回值。

 fork()函数是如何做到返回两个返回值的?

我们还是那上述的例子来所说明。

我们需要注意的是:fork()也是一个函数,那么既然是一个函数,那么这个 fork()就具有自己的函数体,也就是 fork()函数自己的代码块。

此时就有个一个问题了,当一个函数执行到 return 语句的时候,这个函数的主要功能有没有实现呢? 在 fork()函数当中是已经做完了的。

在fork()函数的当中会做很多事情,但是根本上都是在创建一个子进程,这里面 最主要的就是 创建和拷贝数据 PCB,给PCB 的属性值初始化。当 父 子 都有了各自的 PCB 之后,CPU 就可以调度这两个进程了。

但是我们刚刚说了,执行到 return语句之后,fork()函数的主要功能已经实现完了;而且 子进程 和 父进程是共用一个代码,但是return 语句是代码吗?答案肯定是的 。所以,也就意味着 return语句也是父子共享的。

所以,当子进程调度 fork()函数时候,就return返回一个值;父进程调度 fork()函数时候也返回一个值。所以这个 fork()函数就被返回了两次。 因为,当你fork ()函数准备return之前,关于子进程的调度早就执行完了,此时,子进程也允许被CPU调度了。所以,再往后,这个 return语句就是被两个 进程所调用,因为 return 语句是代码,被父子进程所共有,当父子进程运行时,就会调用两次return语句。从而实现返回不同的值。

上述的返回值 Mypid 是如何接受两个返回值的?

在上述描述过,父进程因为 在 使用 "./" 调用父进程的时候。就会带上父进程的 数据和代码,所以,父进程是天生就有数据和代码的,但是,子进程却不是,子进程是在父进程当中在创建的,才创建之时,子进程是没有自己的 数据和代码 的,子进程和父进程共用一个代码。

那么问题来了,子进程的数据从何而来,Mypid 变量用于接收 fork()函数的返回值,但是这个 Mypid 变量只有一个啊。子进程运行也是需要数据和代码的。


所以,如果你有上述的疑问,那么你应该首先明确一点:

在任何平台,进程在运行之时,是具有独立性的

 什么是独立性呢?

比如在 windows 当中创建了 QQ 这个进程 和 微信这个进程,如果其中 QQ 这个进程崩溃了,他不会影响到 微信这个进程;如果 微信崩溃了,不会影响到 QQ 这个进程;这两者之间是没有耦合关系的,也就是我们说的 独立性。

独立的本质是一种割裂关系,各个独立的进程,各自就能做自己的事情,自己就能运行(除非他某项功能需要和其他进程来联系)。


所以,上述 父进程 和 子进程都是 一个独立的进程,那么他们就可以自主运行,不需要 相互之间的依靠。既然是自主运行,因为数据可能被修改,那么两者之间肯定是不能用 相同的数据的,肯定是独立的数据。


读到这里,需要区分的是,代码是共享的,因为代码是不能被进程所修改的;而 数据不是共享的,是独立的,因为数据可能会被修改,肯定不能让父子进程访问同一份数据


子进程要想独立一份数据,就需要拷贝一份数据,拷贝的数据是和 父进程共有的数据,拷贝一份给自己用。除此之外,子进程还有各自 专属数据,是父进程没有的,那么在这个份数据当中也应该创建出来。(这个,为子进程拷贝和创建数据工作是由操作系统完成的。

此时,子进程和父进程就拥有了各自独立的数据,不再冲突。

而在访问不同数据,就是通过 父子进程各自的PCB来完成,调用不同的PCB,就访问各自的进程,不管这个代码是不是共享的,但是只要数据不是共享的就行。


那么,上述也提到了 子进程会拷贝 父进程当中数据;但是有一种情况,拷贝下来的父进程数据,对于子进程当中,大部分数据都是没用的。或者说是,拷贝下来的数据有些是子进程使用的。

所以,操作系统在此处进行了优化

首先,在创建子进程之时,不以上上来就拷贝一份父进程的数据,而是,先让子进程和父进程共享父进程的数据。当子进程像访问的时候,如果子进程要对这个数据进行修改,那么就在内存当中重新开辟一块空间,用于存储子进程要修改的变量的值。

这时,操作系统就告诉子进程,你要修改变量数据,不要在父进程的数据区当中直接修改这个变量的数据;而是,在我给你新开辟的一块空间当中修改这个变量的数据。

如此,往后,只要是子进程想要修改 父进程 数据区当中数据,操作系统就会给 子进程在内存当中新开辟一块空间,用于存储要修改的变量的值。

我们把上述的过程称之为 --父子进程之间的  数据层面的写时拷贝

 在用的时候,在给你开辟一块新的空间,不用就不开辟。


此时我们再来理解上述代码:

 fork()函数由父进程调用返回的值,就直接赋值给 父进程的 Mypid变量;如果是子进程调用 fork()函数,那么此时就会发生写时拷贝;相当于,在此时,有两个Mypid 变量,一个是子进程的,一个是父进程。让父子进程在访问 Mypid 这个变量之时,看到是不一样的值。

父进程在访问 Mypid 变量之时,访问的是父进程数据区当中 Mypid 的值;而 子进程在访问 Mypid 之时访问的是 操作系统写时拷贝 的新空间当中的数据。


如果父子进程被创建好之后,fork()往后,谁先运行呢?

 首先,当我们开始运行一个程序,那么这个进程什么时候开始运行我是不能管理的。比如:在windows当中打开了 QQ,那么我们只是告诉了操作系统,此时要运行 QQ了。但是实际上QQ这个进程什么时候开始,是由操作系统决定的。

因为用户只需要使用这个软件即可,什么时候运行进程是由操作系统自己处理的。

所以,谁先运行是由 调度器决定的。

调度器的工作:

操作系统管理进程是管理 用数据结构存储的各个进程的PCB,那么此时,如果用户想运行某一进程,可能就会把这个进程放到 就绪队列当中(打比方),因为在此之前,可能还有很多的进程在排队。那么此时CPU应该挑选哪一个进程开始执行,这个工作就是有 调度器决定

 因为 cpu 在计算机当中是少量的资源,而进程在运行之时都是要 跑到 cpu 之上去运行的,所以,如何更好的利用好 cpu,让我们感受到 很多个进程在同时被调度,调度器 起了很大作用。

 理解 bash 解析命令的过程

在上述理解了 fork()函数之后,我们来了解一下 bash 。bash 是命令解释器,所以的命令都是要通过 bash 解释来运行的。

像上述的使用的 fork()创建子进程的例子,其中的父进程本身就是一个子进程:

我们查看这个 进程pid:

发现其父进程是 bash 。 

所以,bash 在解析命令,然后运行对应进程的过程当中,为了这个进程在运行之后,不用影响到我 bash 的运行,解释其他命令。所以在 bash 当中也是采用 fork()函数来创建子进程的方式来,创建 bash 命令解析的,运行的子进程。

也就是说:bash 在解析命令之后,通过调用 fork()函数创建子进程;至此会后,bash 就会继续进行解释命令的作用,而 bash 创建的子进程就会去执行 bash 解释出的命令的进程。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/119048.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

golang 工程组件:grpc-gateway 环境安装+默认网关测试

grpc-gateway grpc-gateway 顾名思义是专门是grpc的网关。也是一个protobuf的编译器&#xff0c;是一个proto的插件。 grpc-gateway就是将http请求处理后转发到对应grpc服务上。很多浏览器&#xff0c;或者客户端开箱不支持grpc&#xff0c;只支持传统的restful API。 grpc网关…

【2021集创赛】Arm杯三等奖:基于FPGA的人脸检测SoC设计

本作品参与极术社区组织的有奖征集|秀出你的集创赛作品风采,免费电子产品等你拿~活动。 团队介绍 参赛单位&#xff1a;合肥工业大学 队伍名称&#xff1a;芯创之家 指导老师&#xff1a;邓红辉、尹勇生 参赛杯赛&#xff1a;Arm杯 参赛人员&#xff1a;王亮 李嘉燊 金京 获奖情…

【STM32】RCC时钟模块(使用HAL库)

https://gitee.com/linhir-linhir/stm32-f103-c8/blob/master/STM32%E6%9C%80%E6%96%B0%E5%9B%BA%E4%BB%B6%E5%BA%93v3.5/Libraries/STM32F10x_StdPeriph_Driver/inc/stm32f10x_rcc.h STM32最新固件库v3.5/Libraries/CMSIS/CM3/DeviceSupport/ST/STM32F10x/system_stm32f10x.c…

Centos使用tomcat部署jenkins

jenkins的最新版本已经不在支持jdk8&#xff0c;支持的jdk环境如下&#xff1a; 安装jdk环境 yum -y install java-11-openjdk.x86_64 java-11-openjdk-devel.x86_64安装tomcat tomcat官网 cd /optwget https://dlcdn.apache.org/tomcat/tomcat-9/v9.0.82/bin/apache-tomcat…

【项目管理】如何开展高质量的团队管理

&#x1f449;博__主&#x1f448;&#xff1a;米码收割机 &#x1f449;技__能&#x1f448;&#xff1a;C/Python语言 &#x1f449;公众号&#x1f448;&#xff1a;测试开发自动化【获取源码商业合作】 &#x1f449;荣__誉&#x1f448;&#xff1a;阿里云博客专家博主、5…

0026Java程序设计-中学走读生信息管理系统设计与实现

文章目录 摘要**目录**系统设计开发环境 摘要 目前&#xff0c;中学走读生信息管理系统已经发展成为学校的学生走读管理工作中必不可少的一个组成部分&#xff0c;没有该系统&#xff0c;学生的日常工作就会变得繁琐、效率低下。在信息化的社会发展下&#xff0c;有必要建立一…

【VPX610】 青翼科技基于6U VPX总线架构的高性能实时信号处理平台

板卡概述 VPX610是一款基于6U VPX架构的高性能实时信号处理平台&#xff0c;该平台采用2片TI的KeyStone系列多核DSP TMS320C6678作为主处理单元&#xff0c;采用1片Xilinx的Virtex-7系列FPGA XC7VX690T作为协处理单元&#xff0c;具有2个FMC子卡接口&#xff0c;各个处理节点之…

成功率高达99%!美国伊利诺伊大学研究人员实现镱量子比特无损测量

研究人员通过无损测量镱-171量子比特实现了实时控制。&#xff08;图片来源&#xff1a;网络&#xff09; 金属镱-171原子可能在自然界中最接近完美量子比特。最近的一项研究展示了如何使用它们来进行重复的量子测量和量子比特自旋&#xff0c;这一研究成果将有助于可扩展量子…

蓝桥云课--1024 第 2 场算法双周赛

2-铺地板【算法赛】&#xff08;找规律&#xff09; 一、题目要求 二、思路 &#xff08;1&#xff09;因为每块地砖都是2*3的规格&#xff1a; 1.n<2或者m<2的时候&#xff0c;则不能使用上述规格的瓷砖 No 2.n<3或者m<3的时候&#xff0c;也不能使用上述规格…

30天精通Nodejs--第二天:模块系统与npm

深入了解Node.js&#xff1a;模块系统与npm Node.js作为一款强大的服务器端JavaScript运行环境&#xff0c;模块系统和npm&#xff08;Node Package Manager&#xff09;是其成功的重要组成部分。为我们平时提供了便捷的工具和资源&#xff0c;使得在Node.js平台上构建应用变得…

现在java和大数据选什么?

现在java和大数据选什么&#xff1f; 到底是选择大数据还是JAVA&#xff1f;”相信这个问题困惑着许多转行待定人士和高校专业待选的学生。 在普通人眼里可能会觉得这两个专业或者行业没啥区别&#xff0c;都是IT里的&#xff0c;能有啥大不同。这是第一层。最近很多小伙伴找我…

【Linux】MAC帧协议 + ARP协议

文章目录 &#x1f4d6; 前言1. 数据链路层2. MAC帧格式3. 再谈局域网4. ARP协议4.1 路由器的转发过程&#xff1a;4.2 ARP协议格式&#xff1a; 5. 如何获得目的MAC地址 &#x1f4d6; 前言 在学完网络层IP协议之后&#xff0c;本章我们将继续向下沉一层&#xff0c;进入到数…

深入浅出排序算法之希尔排序

目录 1. 原理 2. 代码实现 3. 性能分析 1. 原理 希尔排序法又称缩小增量法。希尔排序法的基本思想是&#xff1a;先选定一个整数&#xff0c;把待排序文件中所有记录分成个组&#xff0c;所有距离为的记录分在同一组内&#xff0c;并对每一组内的记录进行排序。然后&#xf…

详解预处理(1)

目录 预定义符号 预处理指令#define #define定义符号 #define定义宏 #define替换规则 #和##&#xff08;C语言预处理操作符&#xff09; # ## 带副作用的宏参数 宏和函数的对比 命名约定 在之前我们学习了一个文本文件.c生成一个可执行程序。今天我们详细讲解其中的…

01 # 手写 new 的原理

new 做了什么? 在构造器内部创建一个新的对象这个对象内部的隐式原型指向该构造函数的显式原型让构造器中的 this 指向这个对象执行构造器中的代码如果构造器中没有返回对象&#xff0c;则返回上面的创建出来的对象 手写 new 的过程 new 是一个运算符&#xff0c;只能通过函…

CSS隐藏元素的N种方法,你知道哪一种最适合你?

&#x1f3ac; 江城开朗的豌豆&#xff1a;个人主页 &#x1f525; 个人专栏 :《 VUE 》 《 javaScript 》 &#x1f4dd; 个人网站 :《 江城开朗的豌豆&#x1fadb; 》 ⛺️ 生活的理想&#xff0c;就是为了理想的生活 ! 目录 ⭐ 专栏简介 &#x1f4d8; 文章引言 一、前…

Camtasia2024永久激活码

真的要被录屏软件给搞疯了&#xff0c;本来公司说要给新人做个培训视频&#xff0c;想着把视频录屏一下&#xff0c;然后简单的剪辑一下就可以了。可谁知道录屏软件坑这么多&#xff0c;弄来弄去头都秃了&#xff0c;不过在头秃了几天之后&#xff0c;终于让我发现了一个值得“…

【Linux】gdb调试

目录 进入调试查看代码运行代码断点打断点查断点删断点从一个断点转跳至下一个断点保留断点但不会运行该断点 退出调试逐过程逐语句监视跳转至指定行运行结束当前函数 进入调试 指令&#xff1a;gdb 【可执行文件】&#xff1a; 查看代码 &#xff1a;l 【第几行】如果输入指…

安全设备

一.防火墙 5层应用层 防火墙 4层 udp tcp 协议 华为 厂商 华为 h3 1.区域划分 Dmz 停火区 Untrust 不安全区域 Trust 安全区域 防火墙 默认禁止所有 二.Waf Web 应用防火墙 放到web前面 产品 雷池 绿盟 软件 安…