从 01 开始 从 01 开始
首页
  • 📚 计算机基础

    • 计算机简史
    • 数字电路
    • 计算机组成原理
    • 操作系统
    • Linux
    • 计算机网络
    • 数据库
    • 编程工具
    • 装机
  • 🎨 前端

    • Node
  • JavaSE
  • Java 高级
  • JavaEE

    • 构建、依赖管理
    • Ant
    • Maven
    • 日志框架
    • Junit
    • JDBC
    • XML-JSON
  • JavaWeb

    • 服务器软件
    • 环境管理和配置管理-科普篇
    • Servlet
  • Spring

    • Spring基础
  • 主流框架

    • Redis
    • Mybatis
    • Lucene
    • Elasticsearch
    • RabbitMQ
    • MyCat
    • Lombok
  • SpringMVC

    • SpringMVC 基础
  • SpringBoot

    • SpringBoot 基础
  • Windows 使用技巧
  • 手机相关技巧
  • 最全面的输入法教程
  • 最全面的浏览器教程
  • Office
  • 图片类工具
  • 效率类工具
  • 最全面的 RSS 教程
  • 码字工具
  • 各大平台
  • 校招
  • 五险一金
  • 职场规划
  • 关于离职
  • 杂谈
  • 自媒体
  • 📖 读书

    • 读书工具
    • 走进科学
  • 🌍 英语

    • 从零开始学英语
    • 英语兔的相关视频
    • Larry 想做技术大佬的相关视频
  • 🏛️ 政治

    • 反腐
    • GFW
    • 404 内容
    • 审查与自我审查
    • 互联网
    • 战争
    • 读书笔记
  • 💰 经济

    • 关于税
    • 理财
  • 💪 健身

    • 睡眠
    • 皮肤
    • 口腔健康
    • 学会呼吸
    • 健身日志
  • 🏠 其他

    • 驾驶技能
    • 租房与买房
    • 厨艺
  • 电影

    • 电影推荐
  • 电视剧
  • 漫画

    • 漫画软件
    • 漫画推荐
  • 游戏

    • Steam
    • 三国杀
    • 求生之路
  • 小说
  • 关于本站
  • 关于博主
  • 打赏
  • 网站动态
  • 友人帐
  • 从零开始搭建博客
  • 搭建邮件服务器
  • 本站分享
  • 🌈 生活

    • 2022
    • 2023
    • 2024
    • 2025
  • 📇 文章索引

    • 文章分类
    • 文章归档

晓林

程序猿,自由职业者,博主,英语爱好者,健身达人
首页
  • 📚 计算机基础

    • 计算机简史
    • 数字电路
    • 计算机组成原理
    • 操作系统
    • Linux
    • 计算机网络
    • 数据库
    • 编程工具
    • 装机
  • 🎨 前端

    • Node
  • JavaSE
  • Java 高级
  • JavaEE

    • 构建、依赖管理
    • Ant
    • Maven
    • 日志框架
    • Junit
    • JDBC
    • XML-JSON
  • JavaWeb

    • 服务器软件
    • 环境管理和配置管理-科普篇
    • Servlet
  • Spring

    • Spring基础
  • 主流框架

    • Redis
    • Mybatis
    • Lucene
    • Elasticsearch
    • RabbitMQ
    • MyCat
    • Lombok
  • SpringMVC

    • SpringMVC 基础
  • SpringBoot

    • SpringBoot 基础
  • Windows 使用技巧
  • 手机相关技巧
  • 最全面的输入法教程
  • 最全面的浏览器教程
  • Office
  • 图片类工具
  • 效率类工具
  • 最全面的 RSS 教程
  • 码字工具
  • 各大平台
  • 校招
  • 五险一金
  • 职场规划
  • 关于离职
  • 杂谈
  • 自媒体
  • 📖 读书

    • 读书工具
    • 走进科学
  • 🌍 英语

    • 从零开始学英语
    • 英语兔的相关视频
    • Larry 想做技术大佬的相关视频
  • 🏛️ 政治

    • 反腐
    • GFW
    • 404 内容
    • 审查与自我审查
    • 互联网
    • 战争
    • 读书笔记
  • 💰 经济

    • 关于税
    • 理财
  • 💪 健身

    • 睡眠
    • 皮肤
    • 口腔健康
    • 学会呼吸
    • 健身日志
  • 🏠 其他

    • 驾驶技能
    • 租房与买房
    • 厨艺
  • 电影

    • 电影推荐
  • 电视剧
  • 漫画

    • 漫画软件
    • 漫画推荐
  • 游戏

    • Steam
    • 三国杀
    • 求生之路
  • 小说
  • 关于本站
  • 关于博主
  • 打赏
  • 网站动态
  • 友人帐
  • 从零开始搭建博客
  • 搭建邮件服务器
  • 本站分享
  • 🌈 生活

    • 2022
    • 2023
    • 2024
    • 2025
  • 📇 文章索引

    • 文章分类
    • 文章归档
  • 计算机简史

  • 数字电路

  • 计算机组成原理

  • 操作系统

    • 我的操作系统学习笔记

      • 学习操作系统之前
      • 从操作系统启动开始
      • 操作系统接口
      • 操作系统历史与学习任务
      • 如何高效管理 CPU:并发和进程
      • 如何支持多进程
        • 多进程图像的具体样子
        • 多进程图像的始与终
        • 多进程图像的实际样子
        • 操作系统如何支持多进程
        • PCB+ 状态 + 队列
        • 多进程如何交替
        • 进程切换的是三个部分:队列操作 + 调度 + 切换
        • 多进程之间的相互影响
        • 多进程之间的合作
        • 如何实现进程合作:合理的推进顺序
        • 小结
      • 用户级线程
    • 我的操作系统实验笔记

    • 操作系统网课-王道考研

  • Linux

  • 计算机网络

  • 数据库

  • 编程工具

  • 装机

  • 计算机基础
  • 操作系统
  • 我的操作系统学习笔记
2022-11-09
目录

如何支持多进程

# 5. 如何支持多进程

上一课我们讲了为什么要有多进程图像:因为操作系统要管理好 CPU。CPU 是一个取指执行的自动化部件,管理 CPU 首先得让 CPU 工作起来。我们还讲了执行中的程序和静态程序有很大的区别,讲述进程的概念,为了让 CPU 高效工作,只有一个进程是不够的,必须得是多个进程,交替执行。

‍‍

从今天开始,我们来看操作系统怎么支持多进程图像,主要是两部分:

  • 什么是多进程图像,有没实际的样子,能不能看得到摸得着
  • 为了实现多进程,操作系统应该做些什么事情(这里只是略略的讲,做个铺垫,宏观的论述,后面会详细展开)

​ ‍

# 多进程图像的具体样子

‍ 我们首先来看一看操作系统里的多进程图像大概长什么样子,这个多进程可以看得到吗?从什么时候开始有多进程图像?又是从什么时候结束 ‍ 什么是多进程图像:我们可以简单的在纸上画出来

举个例子,操作系统里有 3 个进程,每个进程有一个 ID,并且还对应一个名字;从用户的角度来看,操作系统中有 3 个进程,分别是 ID=1 的进程,ID=2 的进程,ID=3 的进程。那么用户看到后就知道现在计算机中跑着这 3 个进程,第一个进程可能是 PPT,第二个进程可能是 word 等。

也就是说,从上层用户的角度来看,用户怎么使用计算机?启动了进程,就是开始使用计算机了。而对于操作系统来说,负责记录和管理好这几个进程。怎么记录?用 PCB(Process control block)。通过创建 PCB 记录进程的信息,比如当前执行到哪了,在合适的时候让进程调度执行,合理有序的推进

​ ‍

用户不用关心声卡的信息,也不用关注显存有多大,磁盘有多少,用户只需关注进程和推进的样子,而操作系统负责把这多个进程向前推进。可以看到操作系统是多么重要的一个图像

# 多进程图像的始与终

多进程图像从系统的开始就存在,直到操作系统的关机,这个图像是存在操作系统始终的。

我们前面讲操作系统的启动的时候,知道最后讲的是 main.c 的 main 方法,最后执行的 fork:

//linux-0.11/init/main.c 第138行
if (!fork()) {		/* we count on this going ok */
	init();
}
1
2
3
4

这个 fork 是一个系统调用(我们后面会详细展开来讲)就是启动了一个进程,启动了进程后执行 init,而 init 最后就是执行一个 shell。

所以大家可以看到,操作系统启动后,是如何让用户使用的?创建了一个进程,启动了 shell 让用户使用。而 Windows 的话,就是启动了一个桌面,让用户使用。

启动了 shell 之后,shell 又做了什么事情?大家可以看看 shell 的核心代码:

while(1){{
	scanf("%s", cmd);
	if( !fork() ){
		exec(cmd);
	}
}
1
2
3
4
5
6

也就是说,用户输入命令后,shell 会根据这个命令创建一个进程并执行之。比如用户输入 ls,操作系统就创建一个进程去执行 ls;执行完后 shell 继续工作,一直等待用户的输入。当然在用户的命令中,可能会启动多个进程,这里不再详述。

我们之前讲过,计算机是解决实际问题的,怎么解决呢?执行一个任务,也就是启动一个进程。(侧面说明了多进程图像是多么重要) 而执行的任务的好与坏,我们是通过进程推进的样子来判断的。 ‍ ​

# 多进程图像的实际样子

除了纸上画一画讲一讲,我们能不能实际看看多进程图像的样子?以 Windows 为例,我们可以打开任务管理器(快捷键 Ctrl + shift + ESC,也可以在 win 菜单里输入任务管理器),然后大家可以看到进程的名称,比如 Excel 进程就是对应着我们平时使用的 Excel。当然有些进程不是我们启动的,是操作系统后台自己要用的。

​ ‍

再讲一个小例子,如果在使用计算机的时候发现计算机特别慢,肯定是有些地方不太对劲,我们通常会打开任务管理器,看看哪一个进程对 CPU 的使用率特别高,影响到了其他进程。

即使没学过操作系统的人,可能也会下意识的这样做,实际上这种行为也很正确,非常符合操作系统的核心原理,因为计算机就是靠多个进程来使用的

如果我们现在打开一个 word,只需在菜单里点击 word,那么任务管理器就会有一个 word 进程,我们不用担心内存、显示器、键盘等,我们只需启动一个进程,就可以开始使用 word 了;如果我们杀掉 word 进程,那么 word 就会被关闭

​ ‍

同样的,我们打开任务管理器,也是打开了一个进程

​ ‍

对于 Linux 和 Mac,可以在终端用 top 命令查看进程的信息,比如我用云服务器看到的内容:

Tasks: 138 total,   1 running, 137 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0.8 us,  1.2 sy,  0.0 ni, 98.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem :  3880416 total,   790428 free,  2326216 used,   763772 buff/cache
KiB Swap:  1048572 total,   524540 free,   524032 used.  1283276 avail Mem 

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND                                                                        
18145 root      10 -10  158480  29824   5948 S   4.7  0.8   4250:58 AliYunDun                                                                      
28356 root      20   0 1946356  16380   7144 S   0.7  0.4 558:25.35 fail2ban-server                                                                
    9 root      20   0       0      0      0 S   0.3  0.0 121:11.72 rcu_sched    
1
2
3
4
5
6
7
8
9

可以看到进程的 ID(PID),进程属于哪个用户(root 用户),占用多少 CPU 和内存,进程的名字是什么(最后一列) ‍ 虽然讲解的有些繁琐,但这是必要的,希望大家能形成一个概念:用户使用计算机就是启动了一堆进程,操作系统管理硬件就是管理着这一堆进程。第一个部分就讲到这里。 ‍

# 操作系统如何支持多进程

现在讲第二部分,操作系统为了支持多进程要做什么事情,我们这里先略路的讲,后面的课程会详细的展开。

第一个问题,操作系统如何组织多个进程?

操作系统感知进程全靠 PCB,而组织进程也全靠 PCB,用 PCB 形成一些队列,来组织多进程。为什么多进程必须要组织?因为操作系统只有组织好多个进程,才能管理好,合理有序的推进多个进程

就好比管理学生,可以把不同年纪,不同专业的学生分别用不同的数据结构来保存,

操作系统主要用队列来组织进程。有的进程在等待 CPU,我们可以称之为就绪态的进程,用一个就绪队列来保存;有的进程正在执行,我们也有一个地方存储正在执行的进程的 PCB;还有的进程在等待 IO 事件,我们也可以创建一个磁盘等待队列 ‍ 多进程如何组织:一句话,就是将多个进程对应的 PCB,分别放在不同点地方,有就绪队列,等待队列等。

​ ‍

# PCB+ 状态 + 队列

一个进程的状态,有很多种。比如有的进程在执行中,有的进程在就绪,还有的进程在等待(阻塞态),我们可以把这些进程分类:

​ ‍

就好比银行,不同业务可能有不同的窗口,正在办理业务的就是运行态,而等待叫号的就是就绪态

操作系统也是一样的,为了管理,可以将进程分成不同的状态,然后状态的转换也是由操作系统做的,操作系统做好了这些控制,不就管理好进程了吗?

操作系统可以根据这个状态图,合理的让进程实现状态的转换,推进进程,这样进程就是不断的往前推进,这个状态图也可以看成是进程的推进图,操作系统就是在这些状态进程上,对进程进行控制和管理 ‍

# 多进程如何交替

我们刚才说了操作系统如何组织和管理进程:用 PCB+ 队列 + 状态。第二个就是操作系统如何切换和交替进程,这也是一个很重要的部分,因为切换是多进程图像的核心。如果不能切换的话,就只能一个进程执行完了再执行下一个进程,这就不叫多进程图像了。

我们后面会专门拿出好几讲来说明,切换是比较复杂的,代码也是。这里举个例子,就一个进程开始磁盘读写了,就必须要等磁盘,那么就必须切换,把自己变成阻塞态,然后操作系统就会把它放到阻塞等待队列上;

启动磁盘读写;
pCur.state = ‘W’;
将pCur放到DiskWaitQueue;
schedule();
1
2
3
4

‍ 然后关键是切换函数 schedule。

schedule()
{ 
	pNew = getNext(ReadyQueue);
	switch_to(pCur,pNew);
}
1
2
3
4
5

‍ schedule 做了什么?首先就是得从就绪队列找出下一个进程(用 getNext),然后用 switch_to 完成切换,这里 pCur 就是指当前的进程,pNew 就是下一个要被执行的进程,pCur 和 pNew 都是 PCB。

那么怎么找出下一个进程,就得谈调度了,选好是哪一个进程后,接下来就是具体的切换,比如把 PCB 的内容保存起来,把下一个进程的 PCB 恢复等等

# 进程切换的是三个部分:队列操作 + 调度 + 切换

进程怎么调度,这个话题实际上很深刻,本课程不去讲这种特别深刻特别理论性的东西,我们的课程主要讲一个基本操作系统是怎么运转起来的,那么如果深入讲进程调度,可能专门开一门课,因为进程调度有好多算法,实际上每年还在有操作系统的一些研究,一些国际会议的讨论,但我们讲一个最基本的调度大概怎么做的就可以了。

最基本的调度算法:FIFO,先进先出。非常简单,队列不是有很多进程么,我就选第一个

生活中也有这样的例子,比如在银行办理业务,办理完后,总是选拍在第一个的人去办理业务。

如果我们要提高性能的话,可以引入优先级等,我们后面再展开。

那么切换要怎么做呢?基本思想也很简单,就好比我们看书,突然来了个客人,我们会首先将当前看到的场景记在脑海里,然后去接待客人;等接待完客人后,再将场景恢复到脑海中

对于 CPU 来说,就是将当前进程执行的现场保存起来;怎么保存呢?保存再 PCB 里

switch_to 的部分伪代码:

switch_to(pCur,pNew) {
	pCur.ax = CPU.ax;
	pCur.bx = CPU.bx;
	...
	pCur.cs = CPU.cs;
	pCur.retpc = CPU.pc;
	CPU.ax = pNew.ax;
	CPU.bx = pNew.bx;
	...
	CPU.cs = pNew.cs;
	CPU.retpc = pNew.pc; 
}
1
2
3
4
5
6
7
8
9
10
11
12

‍ 动画示意图如下:就是把当前 CPU 里的信息保存在结构体里即可

保存现场​ ‍

那么下一步就是恢复下一个进程的执行现场。怎么恢复呢?将下一个进程的 PCB 信息恢复到 CPU 里:

操作系统-PCB2 恢复​ ‍

这个事情要精细的控制,是用汇编代码写的。

# 多进程之间的相互影响

多个进程交替执行,还有没有其他注意事项?这里引出多个进程交替执行的时候,还会相互影响,这种影响也是需要处理的。

为什么会相互影响?因为多个进程都在内存中,可能会对同一段内存操作。例如进程 1 对内存地址为 100 的内存单元进行了覆盖(可能是故意或者写错),而内存地址为 100 的地方是进程 2 的代码,这样进程 2 就会崩溃

​ ‍

有的同学可能会问,能不能通过特权级的方式限制?肯定是不行的,特权级主要是用于保护操作系统的。而用户进程,其特权级都是 3,那么访问其他非特权级的内存地址,都是可以的 ‍ 那么怎么限制这种读写?基本思想是通过映射表。这一部分是内存管理的内容,我们在后面很久才会讲到,但这也是多进程图像必须要做到的事情。多个进程在内存中,必须要分离,通过映射表来实现分离。如果不分离,那么进程就会打架。

映射表是内存管理的核心,而内存管理实际上也是为多进程图像服务的,这也再一次说明多进程图像是操作系统的核心。

映射表的具体是怎么做的呢?举个例子,进程要访问内存地址为 100 的内容,这个 100 不是真实的物理内存的地址,操作系统会根据这个映射表,查找出其到底对应哪一个地址,例如 780;通过映射表,将访问限制在进程 1 范围内。进程表访问不了其他进程的内容。

​ ‍

而另一个进程也访问 100,也是根据映射表查找真实的物理内存地址,假设为 1260;虽然两个进程都是访问 100,但映射到物理内存上就是被分开的,这样进程就不会也不能影响其他进程的内存。

只有多个进程能在内存里很好的共存,多个进程才能实现交替。

# 多进程之间的合作

有时候多个进程要合作。比如 word 进程要打印,pdf 进程也要打印,那么 word 和 PDF 就要把打印的内容交给打印进程,比如将内容放到内存的某个地址,而打印进程就去取内容打印。

这个合作也必须要处理,如果不约定好怎么合作,打印就会乱套。

例如,进程 1 往内存 100 存放内容,存放到一半,被调度了,进程 2 开始执行;而进程 2 也放内存 100 放内容,那么进程 1 的内容就会被覆盖;

​

这个就是一个典型的生产者-消费者模型。这个模型我们后面会详细的展开来讲,很有趣也很复杂。

生产者负责生产数据(例如 word 进程,生产打印内容),消费者负责消费数据(例如打印进程,打印完内容后),而生产者和消费者通过共享缓冲区消费数据。

//共享数据
#define BUFFER_SIZE 10
typedef struct { . . . } item;
item buffer[BUFFER_SIZE];
int in = out = counter = 0;


//生产者进程
while (true) {
	while(counter== BUFFER_SIZE) ;   //注意这个空循环体 
	buffer[in] = item;
	in = (in + 1) % BUFFER_SIZE;
	counter++;
}

//消费者进程
while (true) {
	while(counter== 0) ; 	 //注意这个空循环体 
	item = buffer[out];
	out = (out + 1) % BUFFER_SIZE;
	counter--;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

‍ 生产者和消费者怎么合作?首先,缓冲区满了就不应该再放内容,因此有个 counter 变量帮助合作,counter 是当前缓冲区的内容数量,如果 counter == buffer_size 了,生产者就不应该往下放数据,因此用死循环阻塞起来;如果缓冲区没有满,就存放数据,并且将 counter++。消费者同理,没数据就死循环等待,有数据就打印,并将 counter--。

因此,想要合作顺利,counter 的值就必须正确。什么叫不正确呢?如果缓冲区已经慢了,但 counter 的值不等于 buffer_size,那么生产者就会继续往缓冲区放内容,之前的内容就被覆盖了;

而如果多个进程在内存里交替执行,就有可能出现 counter 的值不正确的情况。

举个例子,counter=5,生产者和消费者在对 counter 操作的时候,最终的汇编语言是这样的:

//生产者P
register = counter; 
register = register + 1;
counter = register;


//消费者C
register = counter; 
register = register - 1;
counter = register;
1
2
3
4
5
6
7
8
9
10

‍ 当生产者执行到前面两行代码的时候,突然被切出去了,切换到消费者执行;而消费者也在执行了两行代码后,被切换到生产者执行,在多进程并发的情况下,是有可能发生这种情况的。此时执行的序列如下:

P.register = counter;   //P.register = 5 
P.register = P.register + 1;	//P.register = 6
C.register = counter; 	// C.register = 5
C.register = C.register - 1;  //C.register = 4
counter = P.register; 	//counter=6
counter = C.register;	//counter=4
1
2
3
4
5
6

也就是说,生产者存放了一个内容,而消费者消费了一个内容,那么 counter 应该还是为 5,但目前为 4 了,这样 counter 的值就不对了 ‍

# 如何实现进程合作:合理的推进顺序

操作系统为了支持多进程,必须要实现进程的同步。因此要合理的推进进程,而不是想要切换的时候就切换,只有合适的时候才能切换。

以上个例子为例,消费者对 Counter 操作的时候,必须执行完了那 3 条代码,才能切换出去,中途如果想要切换是不允许的。具体方法就是给 counter 上锁,当消费者想要操作 counter 的时候,发现上锁了,就不对 counter 操作;

等 counter 被生产者操作完了,就会解锁,此时消费者才可以对 counter 操作。这样就实现了进程的同步,操作系统负责推进多个进程,但不是想推就推的,必须合理的推进。 ‍ ​

# 小结

我们主要讲了以下内容:

  • 多进程图像的基本样子:多进程存在于操作系统的始终,可以用任务管理器来看进程的样子。

  • 操作系统如何支持多进程:

    • 首先是如何组织多个进程:用 PCB+ 队列
    • 如何完成进程的切换:PCB 队列 + 调度 + 切换
    • 如何让进程不互相影响:通过内存管理,映射表
    • 如何让进程合作:通过锁实现同步

我们后面会详细展开,课程大纲:

  • 读写 PCB,OS 中最重要的结构,贯穿始终
  • 要操作寄存器完成切换(L10,L11,L12)
  • 要有进程同步与合作(L16,L17)
  • 要有地址映射(L20)

​ ‍

上次更新: 2025/5/9 14:55:39
如何高效管理 CPU:并发和进程
用户级线程

← 如何高效管理 CPU:并发和进程 用户级线程→

最近更新
01
学点统计学:轻松识破一本正经的胡说八道
06-05
02
2025 年 5 月记
05-31
03
《贫穷的本质》很棒,但可能不适合你
05-27
更多文章>
Theme by Vdoing | Copyright © 2022-2025 | 粤 ICP 备 2022067627 号 -1 | 粤公网安备 44011302003646 号 | 点击查看十年之约
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式