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

【Linux篇】:初步理解何为进程--从硬件“原子“到PCB“粒子“的进程管理革命

✨感谢您阅读本篇文章,文章内容是个人学习笔记的整理,如果哪里有误的话还请您指正噢✨
✨ 个人主页:余辉zmh–CSDN博客
✨ 文章所属专栏:Linux篇–CSDN博客

在这里插入图片描述

文章目录

  • 一.认识冯诺依曼体系结构
  • 二.认识操作系统
    • 1.为什么要进行管理?
    • 2.怎么进行管理的
  • 三.进程的初步认识
    • 1.描述进程
    • 2.组织进程
    • 3.查看进程
  • 四.fork函数创建进程(初识)

一.认识冯诺依曼体系结构

在学习进程之前需要先来理解冯诺依曼体系结构和操作系统。

首先来看什么是冯诺依曼体系结构,在我们日常生活中的计算机,比如笔记本,不常见的计算机比如服务器,大部分都是遵循冯诺依曼体系结构,如图所示:

(注意:现阶段只需要关注数据信号,也就是红色的线即可,至于控制信号之后再学习)

在这里插入图片描述

  • 输入设备
    • 功能:将外部信息(程序,数据或者指令)转换为计算机可以识别的形式。
    • 示例:键盘,鼠标,摄像头,麦克风,磁盘,网卡等
  • 输出设备
    • 功能:将计算机处理后的结果转换为人类可以识别的形式。
    • 示例:显示器,播放器,扬声器,磁盘,网卡等

上面这两种设备统称为外设,负责输入和输出,根据示例可以发现,有的设备是纯的输入或者输出,也有即使输入又是输出的设备。

  • 存储器

    • 功能:存储程序指令和运算数据

    • 分类

      主存储器(内存):直接于CPU交互,速度快但是断电后数据丢失。

      辅助存储器(外存):长期存储数据(比如硬盘),速度较慢。

我们日常生活中经常提到的内存其实就是指的是存储器。硬件级别的缓存空间,处于计算机硬件中的核心地位。

  • 运算器
    • 功能:执行算术运算(加减乘除)和逻辑运算(与,或,非等)。
  • 控制器
    • 功能:协调计算机各部件的工作,通过解析指令生成控制信号,对计算机硬件流程进行一定的控制。

上面两个运算器和控制器共同组成中央处理器,也就是我们常说的CPU,其中控制器相当于计算机的大脑,运算器则是CPU的核心部件,直接处理数据。

CPU用来运行各种程序,但是一个程序要想被CPU运行必须要先加载到内存中,这是为什么?

因为根据冯诺依曼体系结构,CPU并不能直接访问外设,也就是输入输出设备,只能直接访问内存,所以一个程序如果被CPU执行,那么一定是已经存储到内存中。此外输入输出设备也只能直接访问内存,也就是写入内存或者从内存中读取。

二.认识操作系统

在刚开始学习操作系统指令的时候,简单地讲解过什么是操作系统。

操作系统是一款对软硬件做管理的软件

1.为什么要进行管理?

操作系统帮助用户管理好下面的软硬件资源,为了给用户提供一个良好的(稳定,安全,高效)运行环境。

注意:操作系统管理好底层的软硬件资源(是手段),为用户提供一个良好的执行环境(是目的)。

操作系统里面,会有各种数据,可是,操作系统不相信任何用户!

所以操作系统为了保证自己数据安全,也为了保证给用户能够提供服务,操作系统以接口的方式给用户提供调用的入口,来获取操作系统内部的数据。

由操作系统提供的接口就是系统调用接口,是操作系统提供的用C语言实现的,自己内部的函数调用,也就是系统调用

所有访问操作系统的行为,都只能通过系统调用完成!

但是系统调用在使用上,功能比较基础,对用户的要求比较高,于是一些开发者对部分系统调用进行适度封装,从而形成了库函数,有了库函数,就有利于更上层用户或者开发者进行二次开发。

库函数就是图中用户部分的用户操作接口层次,是对系统调用的封装,而系统调用则是系统软件部分中的system call层次,是内核提供的接口。这两个的关系是上下层的关系,调用和被调用的关系。

在这里插入图片描述

2.怎么进行管理的

既然操作系统是对软硬件资源进行管理,那又是怎么管理的呢?

这里需要举一个例子,以大学为例。学校里面有校长,有辅导员,有学生。当学校需要举办一个活动时,校长肯定要向各位辅导员下达指令,组织各个班级参加活动,然后辅导员将活动信息传达给学生,然后学生报名参加。

在上面这个例子中,校长向辅导员传达信息(校长就是决策者,也就是管理者),然后辅导员再向学生传达信息执行校长的决策(辅导员就是执行者),学生根据信息参加活动(学生就是被管理者),这个信息传递的过程,其实就是类似于操作系统向驱动程序发送信息,然后驱动程序再向硬件发送信息。所以操作系统就相当于管理者,驱动程序就是执行者,而底层硬件就是被管理者。

但是在大学中,学生很少见到校长,依然能被管理好,所以这里就是结论一在进行管理的时候,管理者和被管理者是不需要见面的

接下来就需要扯远一点,人是怎样辨别认识一个事情或者对象的?比如当你向其他人介绍自己的朋友时,肯定会说我的朋友叫什么,年龄多大,长的怎么怎么样,我们会发现,我们在介绍的其实都是这个人的属性。也就是说,我们都是通过属性来认识新的事物或对象的,当属性越多,这一堆属性的集合,就是目标对象。

回到正题,所以对于一个学生来说,每个学生都有对应的属性,比如说姓名,专业,班级,专业等等,由这些各种各样的属性就组成了学生这个集合体。

这个过程就是描述的过程

struct student{
    char 姓名[];
    char 专业[];
    char 班级[];
    ....
};

所以,对学生做管理,其实就是对学生这个属性结合体做管理,而属性结合体本质其实也就是数据。

因此结论二管理者在不见被管理者的情况下,只要能够得到管理信息,就可以做出管理决策,所以管理的本质:通过对数据做管理,达到对人做管理。

此外,在大学中,校长只有一个,而学生有成千上万个,在这种情况下,又该怎么进行管理?

这时候就要借助到学过的数据结构了,我们可以在学生这个结构体中设置一个指针用来指向下一个学生对象,也就是链表结构。

struct student{
    char 姓名[];
    char 专业[];
    char 班级[];
    ....
    struct student *next;
};

通过链表这个结构,就可以对大量的学生进行管理,新来的学生就获取该学生的属性信息插入到链表中,开除某个学生就找到链表中对应的学生删除,因此就成功地将对学生的管理工作转化为对链表的增删查改!

上面这个过程就是组织的过程

上面说了这么多本质其实就是想说明,对任何的事物做管理都要经过先描述再组织的过程,其实就是面向对象的思想再结合某种数据结构

因此在操作系统中,管理任何对象,最终都可以转化为对某种数据结构的增删查改。

三.进程的初步认识

什么是进程,直接说结论:

一个已经加载到内存中的,正在被CPU运行的程序,叫做进程(有的也叫做任务)

为什么说进程是已经加载到内存中?

因为根据冯诺依曼体系结构(这就是为什么前面要先讲解冯诺依曼体系结构),CPU只能和内存(存储器)交互,所以正在被运行的程序一定是已经加载到内存中的。

而一个操作系统,不仅仅只能运行一个进程,可以同时运行多个进程,所以操作系统必须得对进程做管理

如何做管理?

根据前面举的例子,当然是先描述再组织

1.描述进程

任何事物都有属性,进程也不例外。任何一个进程在加载到内存的时候,形成真正的进程时,操作系统都要先创建描述该进程属性的结构体对象--------PCB(process ctrl block)进程控制块

这个PCB其实就是进程属性的集合,一个struct结构体:

struct PCB{
    进程编号
    进程状态
    优先级
    ....
    相关的指针信息
    .....
};

根据进程的PCB类型,为该进程创建对应的PCB对象。

但是如果光有进程的属性集合PCB,而没有该进程对应的代码和数据,同样无法运行。

就好比在学校中,学生管理系统中光有对应学生的属性信息,而学校内并没有对应的这个学生,怎么进行管理。如果反过来,学校里光有该学生这个人,但是学生管理系统中并没有该学生的属性信息,同样无法管理。

所以同理,在内存中,并不能只存在进程的PCB或者只存在进程对应的代码和数据,而是两者同时存在缺一不可,这时候才是一个完整的进程。

进程=内核数据结构对象(PCB)+对应的代码和数据

注意:上面关于进程的描述只是暂时的,后面会进行修改,现在只能这样理解。

所以这就是为什么上面的PCB结构体中有一个相关的指针信息这个属性,需要用一个指针指向该进程的代码和数据,这样才能通过该进程的PCB属性集合体直接找到对应的代码和数据。

所谓对进程做管理,其实就是对内核PCB做管理(数据),而不是对应的代码和数据做管理

2.组织进程

描述完之后,每一个进程都有一个PCB,只需对内核PCB做管理,但是操作系统内存在多个进程,这时候就需要用某种数据结构来存储这些PCB(数据),比如说链表:

struct PCB{
    进程编号
    进程状态
    优先级
    ....
    相关的指针信息
    .....
    struct PCB *next;(用来指向下一个PCB的地址)
};

通过指针将所有的PCB连接起来,在操作系统中,对进程的PCB做管理就转化成了对链表进行增删改查!

上面这些是操作系统中进程的基本理论,所有的操作系统都以该理论为基础,但是每个操作系统对进程的管理又各不相同。

那么Linux中具体又是怎么做到的

在Linux中task_struct是用来描述进程属性集合的结构体(PCB的一种),是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。

此外,Linux内核中,最基本的组织进程task_struct的方式,是采用双链表组织的。

3.查看进程

进程的信息可以通过系统目录/proc查看

ls /proc指令查看:

在这里插入图片描述

top指令查看CPU正在运行的所有进程:

在这里插入图片描述

ps ajx | grep [目标进程]查看指定进程信息:

在这里插入图片描述

在这里插入图片描述

因为grep同样也是一个指令,指令也是个进程,所以也会显示出该进程的信息。

ps ajx | head -1 && ps ajx | grep [目标进程]显示信息名称查看指定进程信息:
在这里插入图片描述

四.fork函数创建进程(初识)

在进程的PCB中有一个属性叫做标识符(Pid),用来描述该进程的唯一标识符,区别其他进程(可以理解为编号)。

每个进程都有属于自己的pid,如果想要获取当前进程的pid,(因为进程由操作系统管理,不能绕过操作系统直接获得),只能通过系统调用接口获取------getpid()函数,返回当前进程的pid,返回类型是pid_t

每个进程除了有Pid外,还有另一个PPid,表示的是父进程的标识符Pid,同样的获取PPid,也只能通过系统调用获取-------getppid()函数,返回当前进程父进程的pid,返回类型是pid_t

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

指令也是进程,对应的父进程就是bash(命令行解释器),所以PPid一直不变
在这里插入图片描述

上面的例子中通过./运行我们自己的程序,其实就是在创建一个新的进程,只不过是指令级别的,父进程一直会是bash(命令行解释器)。

而接下来要讲解另外一种创建进程的方式-------fork()函数,代码层面的创建子进程。

先来看下面这段代码的运行现象:

在这里插入图片描述

在这里插入图片描述

根据图中的现象,我们可以发现两个判断语句在同时交替执行,为什么会有这种现象?

这是因为,运行fork()函数的当前进程为父进程,在执行fork()函数后,创建了子进程,而fork()函数给父进程的返回值是子进程的Pid,给子进程的返回值是0。所以父进程的id值大于0,执行大于0的判断语句,子进程的id等于0,执行等于0的判断语句。所以出现了两个判断语句同时交替执行的现象。

光是上面的简单解释并不能完全搞清楚fork()函数,所以下面通过四个问题来逐步讲解fork()函数。

1.为什么fork函数要给子进程返回0,给父进程返回子进程的pid?

首先fork函数是为当前运行的进程创建一个子进程,而前面讲解过:进程=内核数据结构对象(PCB)+对应的代码和数据

在创建子进程时,操作系统同样也会为子进程创建一个属于自己的PCB结构体对象,而里面的内容则是以父进程的PCB为模板来进行填充,当然也会有子进程自己的属性,而通过PCB可以找到该进程的代码和数据,因此创建出来子进程后,子进程会和父进程共享之后的代码。(注意,代码和数据并不都是共享的,这里先讲解代码,后面会讲到数据。)因为对于正在运行的程序来说,代码一定不会再被修改,所以父子进程可以共享之后的代码。

如果在创建子进程后有一个输出语句,那么我们看到的现象就是相同的输出语句被执行了两次:

在这里插入图片描述

在这里插入图片描述

因为父子进程会共享之后的代码,所以相同的代码会被执行两次,但是没必要,原本执行一次就可以的代码为什么要执行两次?因此创建子进程一定是为了让父子进程执行不同的事情!所以需要想办法让父子进程执行不同的代码块,返回不同的返回值,就是为了区分让不同的执行流,执行不同的代码块。

子进程返回0表示的是子进程创建成功,而父进程返回子进程的pid是为了让父进程明确之后要管理的子进程是哪一个(如果创建的子进程最后没有被父进程进行管理会变成僵尸进程,这里提一下后面会讲到)。如果子进程创建失败,会直接返回-1。

2.fork函数究竟干了什么?

在讲解上一个问题时其实已经讲过一点fork函数干了什么,用函数的形式简单描述就是:

pid_t fork(void){
    创建子进程
        1.创建子进程PCB
        2.根据父进程PCB中的内容填充子进程的PCB
        3.让子进程和父进程指向同样的代码
        4.父子进程都有独立的task_struct,可以被CPU调度运行了
        .....
        
    return ret;
}

3.fork函数是如何做到返回两次的?

在第一个问题中得出的结论是,创建子进程后,父子进程共享之后的代码,根据这个结论来看第二个问题中的fork函数,在fork函数最后返回时,子进程一定是已经被创建出来了,所以对于最后的返回语句,父子进程会共享该语句,也就是要执行两次,父进程调度运行返回一次,子进程调度运行同样返回一次,所以返回了两次。

4.一个变量怎么会有不同的内容?

前面讲解了对于代码父子进程是共享的,因为程序运行后,代买并不会被修改,所以可以共享。而对于数据来说,在运行的过程中可能会存在修改,如果数据共享,数据原本是属于父进程的,父进程正常修改后,子进程同样要对该数据进行修改,就会导致数据内容发生错误,所以所以对于数据来说,一定不能共享。因此需要为子进程拷贝父进程的数据。

但是如果刚开始一次性的将父进程数据全部拷贝给子进程,并且是大量数据,但是最后并没有发生修改,就会导致资源的浪费。

所以对于数据的拷贝并不是刚开始的时候一次性全部拷贝过来,而是在后续执行代码的过程中,当子进程对数据进行修改时,操作系统会重新开辟一块空间将该数据拷贝过去,以供子进程修改,子进程用多少就拷贝多少。这种拷贝方式就是数据层面的写时拷贝

所以对于pid_t id=fork();来说,父进程返回时,id原本就是父进程的数据可以直接写入;而当子进程返回时,操作系统会重新开辟空间将id拷贝过去,然后子进程再进行写入。相当于相同的变量有两个不同的空间存放不同的数据内容,至于如何根据不同的空间获取到不同的数据内容,后面讲解地址空间的时候再解释,这里现在解释不清。

总结

根据上面的四个问题就可以真正明白fork函数的功能,而对于fork函数创建子进程后,父子进程是谁先被运行的,并不能明确说明是哪一个,因为这是由调度器决定的。CPU决定把哪个进程放到CPU来执行,调度器决定哪个进程该被调度。

现在再来看指令的父进程bash(命令行解释器),其实bash也是通过调用fork()函数创建子进程来执行我们输入的指令。

写入;而当子进程返回时,操作系统会重新开辟空间将id拷贝过去,然后子进程再进行写入。相当于相同的变量有两个不同的空间存放不同的数据内容,至于如何根据不同的空间获取到不同的数据内容,后面讲解地址空间的时候再解释,这里现在解释不清。

总结

根据上面的四个问题就可以真正明白fork函数的功能,而对于fork函数创建子进程后,父子进程是谁先被运行的,并不能明确说明是哪一个,因为这是由调度器决定的。CPU决定把哪个进程放到CPU来执行,调度器决定哪个进程该被调度。

现在再来看指令的父进程bash(命令行解释器),其实bash也是通过调用fork()函数创建子进程来执行我们输入的指令。

以上就是关于进程概念初步的讲解,如果哪里有错的话,可以在评论区指正,也欢迎大家一起讨论学习,如果对你的学习有帮助的话,点点赞关注支持一下吧!!!
在这里插入图片描述


http://www.kler.cn/a/590208.html

相关文章:

  • Spring Cloud Stream - 构建高可靠消息驱动与事件溯源架构
  • Python----计算机视觉处理(Opencv:图像缩放)
  • vulkanscenegraph显示倾斜模型(5.3)-相机
  • 【eNSP实战】基本ACL实现网络安全
  • AI大模型本地化谷云科技全域集成能力重构企业数智化生态
  • Logo语言的链表插入
  • 全栈网络安全-渗透测试-3
  • 物联网(IoT)平台层中 大数据处理过程
  • android开发:android.graphics包的介绍
  • SQL注入:安全威胁的幽灵与防御体系的构建——从经典攻击到智能防护的演进
  • Spring 中使用代理的注解及机制分析
  • matlab 正态分布
  • Flink State 是处理有状态流计算的核心机制,其典型应用场景及具体说明
  • 正则表达式小结
  • Redis-锁-商品秒杀防止超卖
  • HTML深度解读
  • 视频转音频, 音频转文字
  • 物联网(IoT)架构中,平台层的应用与技术
  • Spring Security 教程:从入门到精通(含 OAuth2 接入)
  • 硬件驱动——51单片机:独立按键、中断、定时器/计数器