IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 系统运维 -> 【Linux】进程概念(冯诺依曼体系结构、操作系统、进程) -> 正文阅读

[系统运维]【Linux】进程概念(冯诺依曼体系结构、操作系统、进程)

一、冯诺依曼体系结构

1.1 概念

什么是冯诺伊曼体系结构:

美籍匈牙利数学家冯·诺伊曼于1946年提出存储程序原理,把程序本身当作数据来对待,程序和该程序处理的数据用同样的方式储存。 冯·诺伊曼理论的要点是:计算机的数制采用二进制;计算机应该按照程序顺序执行。人们把冯·诺伊曼的这个理论称为冯·诺伊曼体系结构。

我们常见的计算机,如笔记本。以及我们不常见的计算机,如服务器,大部分都遵守着冯诺依曼体系

image-20220607165521629

任何计算机一定由如下几部分构成:

  • 输入单元:包括键盘, 鼠标,扫描仪, 写板等。
  • 中央处理器(CPU):含有运算器和控制器等。
  • 输出单元:显示器,打印机等。

那么冯诺依曼体系结构的计算机是如何工作的呢?

  1. 外设(输入设备)输入数据时,必须先将数据写入存储器,而存储器本身没有计算能力。

  2. 然后 CPU 会通过某种方式读取存储器中的数据,进行指定的运算和逻辑操作等加工后,然后再将处理完的数据通过某种方式写回到存储器中。

  3. 最后外设(输出设备)再从存储器中读取数据并输出。

如图,计算机工作时,数据的流向:

image-20220607152733155

👉 总结

也就是说,冯诺依曼规定了硬件层面上的数据的流向


思考

在磁盘中编写好的可执行程序(文件),运行的时候,必须先加载到内存中!这是为什么呢?

因为冯诺依曼体系规定!可执行程序是二进制指令,CPU 要执行这些指令,必须先将磁盘中的可执行程序加载到内存中,CPU 才能访问执行这些指令。

分析

存储器的层次结构 中,越往上速度越快,外设最慢 < 主存其次 < 高速缓存 < CPU寄存器,我们可看到,CPU离寄存器最近,离高速缓存也很近,主存(存储器)次之,所以 CPU 间接从主存中访问数据,效率更高。

而让 CPU 直接访问外设(输入或输出设备)肯定是不行的,因为 CPU 特别快,输入输出设备特别慢,导致效率低。

image-20220607154403204

当一个快的设备和一个慢的设备协同工作的时候,整个体系最终的运算效率肯定以慢的为主。

类似木桶理论,当我们让 CPU 直接访问磁盘时,那么木桶的短板的就在磁盘上,整个计算机体系的效率就会被磁盘拖累,这显然不是我们想看到的,所以我们必须把数据写入到存储器中,再让 CPU 一级一级的去访问,而且 CPU 运算的同时,输入 / 输出设备还可以继续将数据写入内存或从内存中读出,这样就可以将 IO 的时间和运算的时间重合,从而提升效率。

image-20220607155149255

👉 总结

所以在数据层面上,CPU不和外设(输入或输出设备)打交道,外设只和存储器打交道。(可以将存储器理解为是 CPU 和所有外设的缓存)


1.2 举个例子理解:硬件层面的数据流

对冯诺依曼的理解,不能只停留在概念上,要深入到对软件数据流理解上,请解释下,从你登录上QQ开始和某位朋友聊天开始,数据的流动过程。从你打开窗口,开始给他发消息,到他看到消息之后的数据流动过程。如果是在QQ上发送文件呢?

在QQ上发送消息,数据的流动过程

电脑联网后,我用键盘敲下要发送的消息 “在吗?”,此时输入设备是键盘,键盘将该消息写入到内存中,CPU 间接从内存中读取到消息,对其进行运算处理后,再写回内存,此时输出设备网卡从内存中读取消息,并经过网络发送到对方网卡,同时输出设备显示器从内存中读取消息并刷新出来,显示在我的电脑上。

我朋友的电脑,输入设备是网卡,接收到消息后,网卡将该消息写入到内存中,CPU 间接从内存中读取到消息,对其进行运算处理后,再写回内存,此时输出设备显示器从内存中读取消息并刷新出来,显示在我朋友的电脑上。

这样我们就知道了硬件层面的数据流

键盘 → 内存 → CPU → 内存 → 网卡 → 网卡经过网络到对方网卡 → 内存 → CPU → 内存 → 显示器

image-20220607152820290

1.3 关于冯诺依曼的总结

  • 这里的存储器指的是内存。
  • 不考虑缓存情况,CPU 能且只能对内存进行读写,不能访问外设(输入或输出设备)。
  • 冯诺依曼规定了硬件层面上的数据的流向。
  • 数据层面 上,CPU 不和外设打交道,外设只和存储器打交道(可以将存储器理解为是 CPU 和所有外设的缓存)。(而在硬件层面上,外设是可以直接给 CPU 发中断)
  • 外设要输入或输出数据只能写入内存或从内存中读出。

补充:CPU和寄存器、高速缓存,以及主存之间的关系


1.4 CPU 工作原理

  1. 冯诺依曼体系结构是现代计算机的基础。在该体系结构下,程序和数据统一存储,指令和数据需要从同一存储空间存取,经由同一总线传输,无法重叠执行。根据冯诺依曼体系,CPU的工作分为以下 5 个阶段:取指令阶段、指令译码阶段、执行指令阶段、访存取数和结果写回。

  2. 取指令(IF,instruction fetch),即将一条指令从主存储器中取到指令寄存器(用于暂存当前正在执行的指令)的过程。程序计数器中的数值,用来指示当前指令在主存中的位置。当 一条指令被取出后,程序计数器(PC、用于存放下一条指令所在单元的地址的地方)中的数值将根据指令字长度自动递增。

  3. 指令译码阶段(ID,instruction decode),取出指令后,指令译码器按照预定的指令格式,对取回的指令进行拆分和解释,识别区分出不同的指令类 别以及各种获取操作数的方法。现代CISC处理器会将拆分已提高并行率和效率。

  4. 执行指令阶段(EX,execute),具体实现指令的功能。CPU的不同部分被连接起来,以执行所需的操作。

  5. 访存取数阶段(MEM,memory),根据指令需要访问主存、读取操作数,CPU得到操作数在主存中的地址,并从主存中读取该操作数用于运算。部分指令不需要访问主存,则可以跳过该阶段。

  6. 结果写回阶段(WB,write back),作为最后一个阶段,结果写回阶段把执行指令阶段的运行结果数据“写回”到某种存储形式。结果数据一般会被写到CPU的内部寄存器中,以便被后续的指令快速地存取;许多指令还会改变程序状态字寄存器中标志位的状态,这些标志位标识着不同的操作结果,可被用来影响程序的动作。

  7. 在指令执行完毕、结果数据写回之后,若无意外事件(如结果溢出等)发生,计算机就从程序计数器中取得下一条指令地址,开始新一轮的循环,下一个指令周期将顺序取出下一条指令。


二、操作系统(operating system)

操作系统被称为计算机的哲学。

2.1 前言

任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。

笼统的理解,操作系统包括

  • 内核 Kernel(操作系统最核心的部分,包含进程管理,内存管理,文件管理,驱动管理等)
  • 其他程序(例如函数库,shell 程序等等)

操作系统的定位

  • 在整个计算机软硬件架构中,操作系统的定位是:一款纯正的 “ 搞管理 ” 的软件。

学习操作系统前,先弄明白两个问题

问题一】:操作系统是什么?

  • 进行软硬件资源管理的软件

问题二】:为什么会存在操作系统?设计操作系统的目的?

  • 方便用户使用,减少了用户使用计算机的成本。

    • 对上,给用户程序(应用程序)提供一个稳定高效的运行环境。

    • 对下,与硬件交互,管理好所有的软硬件资源(充分高效的使用软硬件资源)。


2.2 如何理解 “ 管理 ”:先描述,再组织!

思考:操作系统是一款纯正的 “ 搞管理 ” 的软件,那么究竟什么是管理呢?

人的世界要做的就只有两类事情:1、做决策,2、做执行

假设学校模型里面有三部分人构成,学生,辅导员,校长,他们有着不同的身份。

image-20211115222448045

我们在学校里面很少见到校长,说明管理者和被管理者,一般不见面和直接打交道(就像阿里的员工和马云并不见面和直接打交道),那么校长如何进行管理呢?校方是如何知道你是该学校的学生呢?因为你的《个人信息》在学校的系统中,所以你是这个学校的学生。管理的本质是对「数据」进行管理

举个例子:

比如19级软工专业有50名学生,我们想要给其中特定的一名学生发奖学金,那是不是需要校长跑到该专业学生的宿舍里面挨个挨个问同学们的各科成绩,学分绩点是多少,显然不是的,当他想要做发奖学金这个决策的时候,他只需要通过教学管理系统,拉取19级软工专业50名学生名单,按照学分绩点进行排名,排名后再根据其它的一些要求,综合一批数据做出一个决策:我要给张三同学发奖学金。当我做完决策后,我通知该专业的辅导员过来,让他开个表彰大会,奖励下这位同学。辅导员说:” 好的,校长 “,此时辅导员就开始做执行。

以上就完成了一个管理过程。

1、既然是管理「数据」,就一定先要把《学生信息》抽取出来,而抽取要管理的数据的过程,可以称之为:描述学生。

2、思考:C 语言用什么来「描述」学生呢? —— struct 结构体,如果要管理一万个学生,那就有一万个结构体变量,每个结构体变量里面保存着每一个学生的所有信息。

struct student {   // 描述学生
    char name[10];
    int age;
    double score;
    char addr[100];
    // ...
};

3、如果我们要找到成绩最好的,只需要将其每个同学的成绩拿出来,进行比较即可。

但每个结构体变量之间没有任何关联的话,是不方便进行管理的,你也很难快速找到成绩最好的同学。

这个时候就需要将这些结构体变量「组织」起来,比如在 struct 中包含一些指针信息,将所有的结构体变量链接起来,此时就形成了一个双链表。

image-20220612214225035

4、校长要管理学生,只要有双链表的头指针就行了,如果校长想要开除某个学生,只需要遍历双链表,将该《学生节点》从双链表中删除即可;有新生来,只需要将该《学生节点》插入到双链表中即可。

所以校长并不是单独对一个人进行管理的,而是把学生信息组织起来,对数据结构管理。

6、经过上面的过程,最终我们就将对学生的管理工作,转化成为了对双链表的增删查改操作。


结论

  1. 所有管理的工作,本质上就是对「数据」的管理。
  2. 管理的本质:先描述,再组织

总结】:

我们在实际生活中的管理变成了对某种数据结构下的「结构体变量」的管理,这是操作系统管理的本质。

  1. 描述起来,用 struct 结构体

  2. 组织起来,用链表或其他高效的数据结构(不同的数据结构决定了不同的增删查改的特征和效率,也决定了不同的组织方式)

在计算机中,校长通常指的是操作系统,辅导员可以称为驱动,学生可以称为软硬件。

image-20211116134842657

操作系统不会直接和硬件(比如磁盘,网卡,鼠标)打交道,而是通过驱动程序和硬件打交道,那操作系统怎么去管理硬件呢?—— 先描述,再组织!所以操作系统要描述各种各样的硬件,然后形成特定的数据结构,对硬件的管理,最后变成了对数据结构的管理。

举例:操作系统要管理磁盘,那得要有一个描述硬盘的 struct 结构体,而描述一个事物,通常用的是事物的属性,比如磁盘的大小、磁盘的型号等等;操作系统卸载一个硬件,并不是要把这个硬件从电脑中拆卸走,而是把这个硬件对应的描述信息给删除掉。

所以操作系统为了管理好被管理对象,在系统内部维护了大量的数据结构


2.3 计算机体系层状结构

image-20220613172934378
  1. 硬件部分:遵循冯诺依曼体系结构。

  2. 驱动程序:操作系统中默认会有一部分驱动。如果有新外设,就需要单独安装驱动程序,该驱动程序会通过某种方式将该硬件的信息上报给操作系统,告诉操作系统,多了这个硬件。

    (驱动程序更多是一种执行者的角色)

  3. 操作系统:操作系统最重要的四个功能:进程管理、内存管理、文件管理、驱动管理。

  4. 系统调用接口:操作系统是不信任任何用户的,任何对硬件或者系统软件的访问,都必须通过操作系统的手(好比银行是不信任任何用户的,用户想要取钱存钱,都必须经过银行的手),所以用户对操作系统中资源的访问,都必须调用对应的系统接口。(比如:在 Linux 中执行命令,或运行一个 C 程序,底层都用到了系统接口)。

    系统调用接口,本质是操作系统为了方便用户使用操作系统中的某种资源,给用户提供的一些调用接口。

    但即使这样,系统调用接口用起来也不是特别方便。

    所以一般我们会在系统调用接口上再封装一层(比如:shell 外壳,系统库,部分指令,这些的底层一般都是封装的系统调用接口)。

    不断的封装,也是为了让用户用起来更简单。

    比如:安装 C/C++ 环境时,系统会默认带上 C/C++ 标准库,这些库提供给用户的接口是一样的,但是底层可能不一样,在 windows 中调用的就是 windows 的系统接口,在 Linux 中调用的就是 Linux 的系统接口。

  5. 用户操作接口:底层大都是封装的系统调用接口。

拓展

理解系统调用和库函数:

  • 库函数:语言或者第三方库给我们提供的接口。(实际上我们使用的函数,底层一般就两种情况,要么调用了系统接口,比如 printf;要么没有调用系统接口,比如自己写的 add 函数,自己写的循环等等)

    系统调用:操作系统提供的接口。

  • 在开发角度,操作系统对外会表现成一个整体,但还是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。

  • 系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。


三、进程(process)

操作系统是如何进行进程管理的呢?—— 很简单,先把进程描述起来,再把进程组织起来!

3.1 前言

操作系统能不能一次运行多个程序呢?—— 可以的。

  • 因为运行的程序有很多,所以 OS 需要将这些运行的程序管理起来。
  • 这些正在运行的程序称之为进程。
  • 如何管理呢?—— 先描述,再组织!
    • 操作系统会创建一个描述和控制该进程的结构体。这个结构体称之为进程控制块(PCB,Processing Control Block),里面包含了该进程几乎所有的属性信息,同时通过进程控制块也可以找到该进程的代码和数据。
    • 在 Linux 中,进程控制块就是 struct task_struct 结构体。
    • 描述好所有进程了,还需要将所有进程的 PCB 给组织起来(通过双链表的方式),此时操作系统只需要拿到双链表的头指针,就可以找到所有进程的 PCB。
    • OS 把对进程的管理就转换成了,对数据结构中 PCB 的管理,即对双链表的增删查改操作。

假设这里有一个可执行程序 test,它存储在磁盘上,就是一个普通文件,当我们 ./test 运行此程序,操作系统会做以下事情:

将该程序从磁盘加载到内存中,并为该程序创建对应的进程,申请进程控制块(PCB)。

image-20220613203602899

总结

  1. 为什么要存在 PCB 呢?—— 因为 OS 要对进程进行管理。

  2. 目前对于进程的理解:进程 = 程序(代码 + 数据) + 内核申请的与该进程对应的数据结构(PCB)。


3.2 进程概念

进程概念:

  • 课本概念:程序的一个执行实例,正在执行的程序等等。
  • 内核层面:担当分配系统资源(CPU时间,内存)的实体。

3.3 描述进程:PCB(?)

人类认识事物是通过事物的属性,而计算机中通过进程的属性去描述和认识进程。

那为什么又要用到数据结构呢?因为数据结构是把数据组织起来的艺术,可以把被描述对象的属性集组织起来,而不同的数据结构,时间空间运算特征是完全不一样的,可以满足不同的场景。

所以操作系统中充斥着大量的数据结构,用来组织被管理的对象。

描述进程:PCB

  • 为了描述和控制进程的运行,系统中存放进程的管理和控制信息的数据结构称为进程控制块(PCB,Process Control Block),它是进程实体的一部分,是操作系统中最重要的记录性数据结构。它是进程管理和控制的最重要的数据结构,每一个进程均有一个PCB,在创建进程时,建立PCB,伴随进程的生命周期,直到进程终止时,PCB将被删除。
  • 在 Linux 中进程控制块是 struct task_struct 结构体。
  • task_struct 是 Linux 内核中的一种数据结构,它会被装载到 RAM(内存) 里并且包含着进程的信息。

PCB 的内容(?)

思考:PCB 如何描述进程呢?—— 通过进程属性。

task_struct 有以下进程属性保存在过程控制块中,并随进程的状态而变化:

  • 标识符(PID):描述本进程的唯一标识符,用来区别其他进程。

  • 状态(state):指进程目前的动作,任务状态,退出代码,退出信号等。

  • 优先级(priority):在资源有限的前提下,确立多个进程中谁先访问资源,谁后访问资源。

    比如食堂只有一个窗口,一次只能给一个人打饭,所以我们需要排队,而排队的本质就是在确立优先级,决定你是先吃饭还是后吃饭。而插队的本质就是在更改优先级。

  • 程序计数器:程序中即将被执行的下一条指令的地址。

    简单一点来理解,CPU 的核心工作流程是:取指令、分析指令、执行指令。

    进程在运行,实际上是 CPU 在执行该进程的代码,那 CPU 如何得知应该取进程中的哪行指令呢?—— 在 CPU 中有一个寄存器叫做 EIP,这个寄存器通常被称为 PC 指针,保存着当前正在执行指令的下一条指令的地址。

    image-20220614165920397

    如果某个进程没有跑完,不想运行时,可以把 EIP 中的内容保存进这个进程的 PCB 中,方便后面恢复运行(这样说只是为了方便理解,实际上并不是这么简单的)。

  • 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。

    CPU 只认识 PCB,不认识程序代码和数据。

    可以理解成,通过 PCB 中的内存指针,可以帮我们找到该进程对应的代码和数据。

  • 上下文数据:进程执行时处理器的寄存器中的数据。

    image-20220614170227030
  • I/O 状态信息:包括显示的 I/O 请求,分配给进程的 I/O 设备和被进程使用的文件列表。

  • 记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。

    比如:调度一个进程,该进程运行多长时间了,累计被切换多少次了等等。

    记账信息是可以指导操作系统去做某些任务的。(打个比方:假设一个进程被调度了 50s,一个进程被调度了 5s,两个进程优先级一样,那么下次调度时,应该调度哪个进程呢,一般是时间短的。)

  • 其他信息。

image-20220614114314943

3.4 查看进程信息

  • 使用 ps ajx (a:所有,j:任务,x:把所有的信息全部输出)

    一般搭配管道进行使用,如:ps ajx | head -1 && ps ajx | grep test,其中 ps ajx | head -1 是把 ps ajx 输出的信息中的第一行信息(属性列)输出。

    [ll@VM-0-12-centos 8]$ ps ajx | head -1 && ps ajx | grep test
     PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND # 属性列
    26837 27406 27406 26837 pts/1    27406 S+    1001   0:00 ./test
    22628 27923 27922 22628 pts/0    27922 R+    1001   0:00 grep --color=auto test 
    # 第三行是grep进程
    
  • 使用 top 命令实时显示进程(process)的动态。

  • 通过 /proc 系统文件目录查看。

    image-20220614112232778

    如果要进一步查看 pid 为 2559 的进程信息,查看 /proc/2559 文件目录即可。

    image-20220614113456653

    思考:操作系统中的 1 号进程是什么呢?在 root 下查看下 1 号的进程信息:

    image-20220614113508774

3.5 获取进程 ID

通过系统调用接口获取进程标识符:

  • 进程 ID(PID)
  • 父进程 ID(PPID)
#include <sys/types.h>
#include <unistd.h>

// pid_t是无符号整数
pid_t getpid(void);  // 函数说明:返回正在调用进程的进程ID
pid_t getppid(void); // 函数说明:返回正在调用进程的父进程ID

举个小例子:

image-20220614111808901

补充:

shell 是命令行解释器(command Interpreter)

shell 是对所有外壳程序的统称,而 bash 是某一个具体的 shell。bash也是许多 Linux 发行版的默认 shell。

在执行命令的时候,一般情况下,往往不是由 bash 来解释和执行,而是由 bash 创建子进程,让子进程去执行。


3.6 进程切换 & 上下文数据(?)

image-20220614211315615

进程切换

  1. 进程在 CPU 上运行,并不是一直运行到进程结束。每个进程都有一个运行时间单位:时间片。

    时间片:从进程开始运行直到被抢占的时间。

    比如:进程 1 运行了 50ms,即使进程 1 没有运行完,但它的时间片耗尽了,必须剥离此进程,让出 CPU,切换下一个进程运行。

  2. 一般情况下,进程让出 CPU,进行进程切换,有几种情况:

    1. 来了一个优先级更高的进程(要求:OS 必须支持抢占)。
    2. 时间片到了。
  3. 操作系统允许同时运行多个进程,但事实上,一个单核 CPU 永远不可能真正地同时运行多个任务,这些进程 “ 看起来像 ” 同时运行的,实则是通过进程快速切换的方式,在一段时间内,让所有的进程代码都得到推进,这就是「并发」,但由于时间片通常很短(在 Linux 上为 5ms-800ms),用户不会感觉到。

    多核 CPU、或者多个 CPU,允许多个进程同时执行,这就是「并行」。

    大多数操作系统是并发和并行在同时起作用。

上下文数据

  • 进程在 CPU 上运行,CPU 寄存器上会产生很多临时数据,当一个进程被切换时,这些数据是需要被保存的,而这些数据被称为进程的上下文数据。

    image-20220614170227030

    举个生活中的例子:

    张三大一上完后,家中有事想要休学一年,给学校提出申请,保留学籍一年,这时才能正常离开学校。一年后,张三再次回到学校,给学校提出申请,恢复学籍,这时才能继续正常上学。

上下文数据的「保存」和「恢复」

  • 上下文数据的「保存」:当一个进程在运行中,因为某些原因(比如时间片到了),需要暂时停止运行,让出 CPU,此时进程需要保存好自己所有的临时数据(即当前进程的上下文数据)到对应的 PCB 中,保存的目的是为了恢复。
  • 上下文数据的「恢复」:当这个进程又被切换回来时,或者切换到下一个新进程运行时,只需要把该进程的 PCB 中的「上下文数据」重新写入到 CPU 寄存器中,即可恢复运行。

进程切换最重要的一步就是

  • 进行硬件上下文的保存。

运行队列

  • 假如当前操作系统中,有 4 个进程是处于可运行状态的,操作系统会形成一个运行队列。

  • 每一个 PCB 用全局的链表连起来,其中可能有若干处于可运行状态的进程,同时也属于运行队列。

  • CPU 要执行任务时就从这个运行队列中寻找就行了。

  • 当 Linux 内核要「寻找」一个新的进程在 CPU 上运行时,必须只考虑处于 可运行状态的进程,(即在 R 状态的进程),因为扫描整个进程链表是相当低效的,所以引入了容纳 可运行状态的进程 的双向循环链表,也叫运行队列(runqueue)。

    运行队列容纳了系统中所有可以运行的进程,它是一个双向循环队列

    img
  • 该队列通过 task_struct 结构中的两个指针 run_list 链表来维持。队列的标志有两个:一个是 “ 空进程 ” idle_task、一个是队列的长度。

  • 操作系统为每个进程状态管理各种类型的队列。与进程相关的 PCB 也存储在相同状态的队列中。如果进程从一种状态转移到另一种状态,则其 PCB 也从相应的队列中断开,并被添加到进行转换的另一个状态队列中。

  • 所以 PCB 是可以被列入多种数据结构内的。比如 PCB 在被调度的时候,以及在等待某种资源的时候,会被从调度队列移入或移出,包括等待某种资源的等待队列。


3.7 创建进程:fork

① 系统调用接口 fork 介绍

1、平时创建进程一般是通过 ./myproc 运行某个存储在磁盘上的可执行程序来创建。

2、而我们还可以通过系统调用接口来创建进程:

#include <unistd.h>

// pid_t是无符号整数
pid_t fork(void); // fork函数功能:创建一个子进程

函数说明:

  • 通过复制调用进程创建一个新进程。

  • fork 有两个返回值。

  • 父子进程代码共享,数据各自私有一份(采用写时拷贝)。


先看一个小例子:

fork 之后,如果不做任何的分流,fork 下面的所有代码是被父子进程共享的:

image-20220615105631679

此时查看进程:

image-20220614221244247

② fork 的两种理解

  1. **站在程序员的角度 ** 👇:

    父子进程共享用户代码(代码是只读的,不可写),而用户数据各自私有一份为了不让进程互相干扰),采用写时拷贝技术。

    打开 windows 的任务管理器,可以看到有很多进程,假如我把微信进程关掉,会不会影响到QQ进程呢?—— 不会!

    总结:操作系统中,所有进程是互相独立的,进程具有独立性!—— 为了不让进程互相干扰!

    注意:fork 之后子进程会被创建成功。然后父子进程都会继续运行,但谁先运行,是不确定的,由系统调度优先级决定。

  2. 站在内核的角度 👇:

    fork 之后,站在操作系统的角度,是不是系统多了一个进程?—— 是的。

    目前对于进程的理解:进程 = 程序(代码 + 数据) + 内核申请的与该进程对应的数据结构(PCB)。

    fork 创建子进程,通常以父进程为模板,其中子进程默认使用的是父进程的代码和数据(写时拷贝)。

    既然多了一个进程,OS 就会为子进程创建新的 PCB,并把父进程 PCB 中的部分内容拷贝过来。


③ fork 的常规用法

我们创建子进程的目的是为了让子进程给我们完成任务,所以 fork 之后通常要用 if 进行分流,让父子进程执行不同的代码,实现一个并行的效果。(比如父进程播放音乐,子进程下载文件)

通过 fork 的「两个返回值」来进行分流:

  • 如果 fork 执行成功,在父进程中返回子进程的 pid,在子进程中返回 0。失败时,在父进程中返回 -1,不创建子进程,并适当地设置 errno。

    #include<stdio.h>  
    #include<sys/types.h> // getpid, getppid  
    #include<unistd.h>    // getpid, getppid, fork
    
    int main()  
    {  
        printf("I'm a father: %u\n", getpid());
        pid_t ret = fork();
        if (ret == 0) {  
            // child process
            while (1) {
                printf("child process, pid:%u, ppid:%u\n", getpid(), getppid());
                sleep(1);
            }
        }
        else if (ret > 0) {
            // father process
            while (1) {
                printf("father process, pid:%u, ppid:%u\n", getpid(), getppid());
                sleep(1);
            }
        }
        else {
            // failure
        }
        return 0;            
    }
    

    站在语言的角度,是不可能同时进入两个执行流的,既进入 if 也进入 else if 的,也不可能同时执行两个死循环。

    但实际的运行结果:

    image-20220615110719939

④ 理解 fork 的返回值(浅层理解)

问题一】:fork 为什么会有「两个返回值

  • 我们调用一个函数时,这个函数准备 return 了,请问这个函数的功能执行完成了吗?—— 执行完了!

    画图分析 fork 函数:

    image-20220615115230539

问题二】:为什么在父进程中返回子进程的 pid,在子进程中返回的是 0 呢?

  • 在人类世界里每个小孩只有一个亲生父亲,而父亲可以有多个孩子。

    所以儿子找父亲是特别简单的,是唯一的;而父亲为了更好的找孩子,需要给每个孩子标识,并且记住他。(比如:张三、张四、张五、…)

  • 所以在父进程中需要返回子进程的 pid,因为得让父进程知道自己的子进程(儿子)是谁。

    而子进程只需要知道自己被创建成功了就行了,所以在子进程中返回 0 就够了。

问题三】:如果创建多个子进程呢?

  • 通过循环创建,下面这段代码并不完善,只是为了简单理解如果创建多个子进程:

    #include<stdio.h>
    #include<stdlib.h>    // exit
    #include<sys/types.h> // getpid, getppid  
    #include<unistd.h>    // getpid, getppid, fork, sleep
    
    int main()
    {
        // 创建5个子进程
        for (int i = 0; i < 5; i++) {
            pid_t ret = fork();
            if (ret == 0) {
                // child process
                printf("child%d, pid:%u, ppid:%u\n", i, getpid(), getppid());
                sleep(1);
                exit(1); // 子进程退出
            }
        }
    
        getchar(); // getchar()目的是不让父进程退出,则无法回收子进程。
    
        return 0;
    }
    

    运行结果:成功创建了 5 个子进程。但程序会一直卡在这里,不会自己退出。

    image-20220615154101579

留下两个疑问】:

为什么上述代码中,fork 的返回值 ret 有两个值,既等于 0,又大于 0 呢?

fork 之后,父子进程如何做到共享用户代码,如何做到用户数据各自私有的呢?

  • 这两个问题学习了进程地址空间就能够很好的理解了。

3.8 进程状态(state)(?)

一个进程的生命周期可以划分为一组状态,这些状态刻画了整个进程。进程状态即体现一个进程的生命状态。

操作系统描述的状态,放在任何操作系统中都是这样的:

image-20220615155755076

但操作系统描述的状态,是属于一种整体宏观的描述。

所以我们还需要进一步来学习具体一种操作系统,比如 Linux 中的进程状态。


① Linux 内核源码中的进程状态

为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在 Linux 内核里,进程有时候也叫做任务)。

Linux Kernel 源码下载地址:The Linux Kernel Archives

下面的状态在 kernel 源代码(2.6版本)里定义:

/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char* const task_state_array[] = {
    "R (running)",      /* 0 */
    "S (sleeping)",     /* 1 */
	"D (disk sleep)",   /* 2 */
	"T (stopped)",      /* 4 */
	"t (tracing stop)", /* 8 */ // 比如调试程序打断点,在断点处停下来的状态
	"Z (zombie)",       /* 16 */
	"X (dead)",         /* 32 */
};
  • R 运行状态(running):并不意味着进程一定在运行中,它表明进程要么在运行中,要么在运行队列里。
  • S 睡眠状态(sleeping):意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠 interruptible sleep)。
  • D 磁盘休眠状态(disk sleep):有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待 IO 的结束。
  • T 停止状态(stopped):可以通过发送 SIGSTOP 信号给进程来停止进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
  • Z 僵尸状态(zombie)
  • X 死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
image-20220616103445998

② R 状态(running)

思考:一个进程是 R 状态,它一定在 CPU 上面运行吗?

  • 不一定,进程在运行队列中也是 R 状态。如果一个进程想被 CPU 运行,就必须处在 R 状态才行。
  • R 状态是:可运行状态。我准备好了,可以被调度。

思考:为什么该进程的状态是 S 状态,不是 R 状态呢?

image-20220615164454205
  1. 因为该进程大部分时间都在休眠(sleep(1);
  2. 因为 printf 是往显示器上打印,涉及到 IO,效率比较低,该进程需要等待操作系统把数据刷新到显示器中。

所以:该进程绝大多数时间都在休眠,只有极少数的时间在运行,所以很难看到该进程处在 R 状态。

那如何可以看到该进程是 R 状态呢?写一个空死循环 while (1) {} 就可以看到了。


③ S 睡眠状态(sleeping)和 D 磁盘休眠状态(disk sleep)

S:休眠状态(sleeping)(浅度休眠,大部分情况)

  • 表示进程虽然是一种休眠状态,但随时可以接受外部的信号,处理外部的请求,被唤醒。

D:磁盘休眠状态(disk sleep)(深度休眠

  • 比如:进程 A 想要把一些数据写入磁盘中,因为 IO 需要时间,所以进程 A 需要等待,但因为内存资源不足,在等待期间进程 A 被操作系统 kill 掉了,而此时磁盘因为空间不足,写入这些数据失败了,却不能把情况汇报给进程 A,那这些数据该如何处理呢?很可能导致这些数据被丢失。操作系统 kill 掉进程 A 导致了此次事故的发生。

  • 所以诞生了 D 状态,不可以被杀掉,即便是操作系统!只能等待 D 状态自动醒来,或者是关机重启。

👉 总结

S 状态和 D 状态都是一种等待状态,因为某种条件没被满足。

比如:QQ 进程想要给网卡发消息,但网卡太忙了,所以可以把 QQ 进程设置成休眠状态,等网卡闲了再把QQ进程唤醒,去发消息。

👉 补充

查看进程状态时,会看到 S+ 状态和 S 状态,那两个有什么区别吗?

  • S+ 状态:表示前台进程。(前台进程一旦运行,bash 就无法进行命令行解释,使用 ctrl + c 可以终止前台进程)
  • S 状态:表示后台进程。(后台进程在运行时,bash 可以进行命令行解释,使用 ctrl + c 无法终止后台进程)

④ T 停止状态(stopped):了解即可

kill 命令:可以向目标进程发信号

[ll@VM-0-12-centos 9]$ kill -l
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
 6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX	

举个例子:

  • 我们给进程发 19 号信号 SIGSTOP,可以让进程进入 T 停止状态。停止运行。

  • 我们给进程发 18 号信号 SIGCONT,可以让进程停止 T 停止状态。恢复运行。

image-20220615210147751

⑤ Z 僵尸状态(zombie)-> 僵尸进程

前言:

我们要知道,进程退出,一般不是立马就让操作系统回收进程的所有资源:

  • 因为创建进程的目的,是为了让它完成某个任务和工作,当它退出时,我们得知道它把任务完成的怎么样。所以需要知道这个进程是正常还是异常退出的。如果是正常退出的,那么交给进程的任务有没有正常完成呢?

  • 所以,进程退出时,会自动将自己的退出信息,保存到进程的 PCB 中,供 OS 或者父进程来进行读取。

    • 进程退出但父进程还没有读取,进程此时就处于僵尸状态。
    • 读取成功后,该进程才算是真正的死亡,变成 X 死亡状态。

僵尸状态的概念:

  • 僵死状态(Zombies)是一个比较特殊的状态。当子进程退出,并且父进程没有读取到子进程退出时的返回代码时就会产生僵死(尸)进程。(父进程使用系统调用 wait() 让 OS 回收子进程)
  • 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出代码。
  • 所以:只要子进程退出,父进程还在运行,但父进程没有读取到子进程状态,子进程就会进入 Z 状态

僵尸进程例子

#include<stdio.h>
#include<stdlib.h>    // exit
#include<sys/types.h> // getpid, getppid  
#include<unistd.h>    // getpid, getppid, fork, sleep

int main()
{
    // 创建5个子进程
    for (int i = 0; i < 5; i++) {
        pid_t ret = fork();
        if (ret == 0) {
            // child process
            printf("child%d, pid:%u, ppid:%u\n", i, getpid(), getppid());
            sleep(1);
            exit(1); // 子进程退出
        }
    }

    getchar(); // getchar()目的是不让父进程退出,则无法回收子进程。

    return 0;
}

运行结果:成功创建了 5 个子进程。但程序会一直卡在这里,不会自己退出。

image-20220615154101579

观察子进程状态的变化:5 个子进程退出后,因为父进程没有进行回收,都变成了僵尸状态。

image-20220615215336741

⑥ 僵尸进程的危害

  • 僵尸进程是一种问题,必须得得到解决,否则会导致内存泄漏。
  • 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就会一直处于 Z 状态。
  • 退出状态本身就是要用数据维护,也属于进程的基本信息,所以保存在 task_struct(PCB) 中,换句话说,Z 状态一直不退出,PCB 就要一直维护。
  • 如果一个父进程创建了很多子进程,但不回收,就会造成内存资源的浪费,因为数据结构要占用内存,定义一个 task_struct(PCB) 结构体变量要在内存的某个位置开辟空间。这个就是内存泄漏。
  • 如何避免?—— 进程等待。

⑦ 进程状态总结

现在再来回看操作系统描述的状态,分别对应的是 Linux Kernel 中的哪一种进程状态呢?

image-20220615155755076
  • 就绪 / 运行:R 状态
  • 阻塞:S / D / T 状态
  • 退出:Z / X 状态

思考几个问题:

  • 如果一个进程是 D 状态,能够被 kill 杀掉吗?—— 不能,因为进程在深度休眠等待资源。
  • 如果一个进程是 Z 状态,能够被 kill 杀掉吗?—— 不能,因为这种进程已经死了。

⑧ 孤儿进程

  • 父进程先退出,子进程就称之为 “ 孤儿进程 ”,孤儿进程是一种特殊的进程。

  • 孤儿进程会被 1 号 systemd 进程领养,孤儿进程退出时,由 1 号 systemd 进程回收。

    (注:不同的系统版本,1 号进程的名称可能不一样,比如 centos 6.5 的 1 号进程叫 initd)

孤儿进程例子

#include<stdio.h>
#include<stdlib.h>    // exit
#include<sys/types.h> // getpid, getppid  
#include<unistd.h>    // getpid, getppid, fork, sleep

int main()
{
    // 孤儿进程演示
    if (fork() > 0) {
        // father process
        sleep(3);                          // 父进程休眠3s后退出
        printf("father process exits!\n");
        exit(0);
    }

    while (1) {   // 子进程将执行这段代码
        printf("child process, pid: %u, ppid: %u\n", getpid(), getppid());
        sleep(1);
    }
    return 0;
}

运行结果:

image-20220616112628181

观察子进程状态的变化:

image-20220616113009256

1 号进程:

image-20220616113133556

3.9 进程优先级(priority)

思考:优先级 vs 权限,两者有什么区别呢?

  • 优先级:在资源有限的前提下,确立多个进程中谁先访问资源,谁后访问资源。
  • 权限:决定能不能得到某种资源。

① 进程优先级的概念

  • CPU 资源分配的先后顺序,就是指进程的优先级(priority)。
  • 优先级高的进程有优先执行权利。配置进程优先级对多任务环境下的 Linux 很有用,可以改善系统性能。
  • 还可以把进程运行到指定的 CPU 上,这样一来,把不重要的进程安排到某个 CPU,可以大大改善系统整体性能。

② 查看进程优先级:PRI

使用命令 ps -al 查看当前进程的信息:

image-20220616171725444
  • UID:代表执行者的 ID,通过命令 ll -n 可以查看。

    在 Linux 中,标识一个用户,不是通过用户名来标识的,而是通过用户的 UID。

    计算机比较善于处理数据,UID 是给计算机看的,UID 对应的用户名是方便给人看的。

    比如 QQ 可以随意更改昵称,那就说明昵称不是唯一标识这个 QQ 用户的,而是通过 QQ 号。

  • PRI:表示这个进程可被执行的优先级:

    • 其值越小,优先级越高,越早被执行。
    • 其值越大,优先级越低,越晚被执行。
  • NI:nice 值,表示进程可被执行的优先级的修正数值:[ -20, 19 ],一共 40 个级别。

  • 进程新的优先级:PRI(new) = PRI(old, 默认都是 80) + nice

注意

  • 优先级不可能一味的高,也不可能一味的低。因为 OS 的调度器也要考虑公平问题。

  • 进程的 nice 值不是 进程的优先级,他们不是一个概念,但是进程 nice 值会影响到进程的优先级变化。


③ 调整优先级:NI

通过 top 命令(类似于 windows 的任务管理器)调整优先级:

  • 执行 top 命令后,按 r 键,输入进程的 PID,输入 nice 值。
image-20220616202223335

注意

  • 每次输入 nice 值调整进程优先级,都是默认从 PRI = 80 开始调整的。
  • 输入的 nice 值如果超过 [ -20, 19 ] 这个范围,默认按照最左/最右范围来取的。

思考

为什么每次都要默认从 PRI = 80 开始调整呢?

  • 有一个基准值,方便调整。
  • 在设计上,实现比较简单。

为什么 nice 值的范围是 [ -20, 19 ] 呢?

  • 是一种可控状态,保证了进程的优先级始终在 [ 60, 99 ] 这个范围内,保证了 OS 调度器的公平。但公平不是平均!根据每个进程的特性尽可能公平的去调度它们,而不是指每个进程的调度时间必须完全一样。

⑤ 补充文章

补充文章:Linux的进程优先级 NI 和 PR - 简书 (jianshu.com)


4.0 补充几个概念

  • 竞争性:系统进程数目众多,而 CPU 的资源很少,甚至只有一个,所以进程之间是具有竞争属性的。为了更高效的完成任务,更合理的竞争相关资源,便有了优先级
  • 独立性:多进程运行,需要独享各种资源,多进程运行期间互不干扰。(这也是 OS 设计进程的一个原则)
  • 并发:多个进程在一个 CPU 下采用进程切换的方式,在同一段时间内,让多个进程都得以推进。(描述的时间段)
  • 并行:多个进程在多个 CPU 下同时运行。(描述的是时刻,任何一个时刻,都可能有多个进程在运行)

  系统运维 最新文章
配置小型公司网络WLAN基本业务(AC通过三层
如何在交付运维过程中建立风险底线意识,提
快速传输大文件,怎么通过网络传大文件给对
从游戏服务端角度分析移动同步(状态同步)
MySQL使用MyCat实现分库分表
如何用DWDM射频光纤技术实现200公里外的站点
国内顺畅下载k8s.gcr.io的镜像
自动化测试appium
ctfshow ssrf
Linux操作系统学习之实用指令(Centos7/8均
上一篇文章      下一篇文章      查看所有文章
加:2022-07-20 19:20:33  更:2022-07-20 19:21:59 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/18 20:11:06-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码