一、冯诺依曼体系
我们常见的计算机,如笔记本。或者不常见的计算机,如服务器,大部分都遵守冯诺依曼体系。
从输入到输出的顺序是按照上面的数字顺序的。
关于冯诺依曼体系:
- 这里的存储器指的是内存
- 不考虑缓存情况,这里的 CPU 能且只能对内存进行读写,不能访问外设(输入或输出设备)
- 外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取。
- 一句话,所有设备都只能直接和内存进行交互
为什么会有以上的规定呢?首先 CPU 这个设备,它处理数据的速度是非常快的,然后是内存,再到就是各种外设(磁盘);在数据层面上,CPU 一般是不和外设直接交互的,因为访问磁盘的速度是非常慢的,会导致整机效率太低了;因此,因为 CPU 的特性,CPU 的造价是很高的,以 CPU 为中心,距离 CPU 越近,存储效率越高,造价越贵!所以基于冯诺依曼体系结构的计算机,本质上是利用比较少的造价,做出来效率不错的计算机!
二、操作系统
任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。什么是操作系统呢?首先,操作系统是一款软件,而且它是进行软硬件资源管理的软件。
我们宏观地看待从用户到底层硬件的交互如下图:
简单简述一下上面的过程:首先,我们用户输入的指令,会被 shell外壳 (Linux 中是 bash)作为 “媒介” 接收,然后 shell外壳 会将我们的指令调用系统调用接口,然后操作系统会对我们相应的指令执行相应的操作。
那么为什么要有操作系统呢?原因是因为操作系统是将软硬件资源管理起来的好手段,给用户提供良好的、稳定的、高效的、安全的使用环境。
所以,既然操作系统可以管理软硬件资源,那么它的内部一定存在大量的数据对象和数据结构;因为它要对大量的数据进行管理,就可以将数据结构化,对结构化的数据进行管理。在这里可以将操作系统比喻成管理者,而驱动程序和底层硬件都是被管理者。
那么操作系统如何对它们进行管理呢?首先,对一个事务进行管理,我们首先要对它进行建模,即先描述,再组织;先描述,就要先对它的属性进行分析,重要属性的分析就代表这个事务;所以,操作系统要管理某个资源,就要先对它描述,即分析它的属性,对它建模,再进行组织,组织后进行管理。假设操作系统要管理某一个资源,可以将它描述成一个链表,那么就可以看成操作系统对这个链表进行资源管理。
总结,计算机管理硬件/操作系统管理资源
- 描述起来,用 struct 结构体
- 组织起来,用链表或其他高效的数据结构
- 系统调用和库函数概念
在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。
系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。
三、进程
1. 基本概念
进程概念: 所谓进程,简单来说就是在磁盘上的可执行程序加载、拷贝到内存中,就形成了进程,也就是正在执行的程序。
2. 描述进程 - PCB
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合,称之为 PCB(process control block),Linux 操作系统下的 PCB 具体是:task_struct.
在 Linux 中描述进程的结构体叫做 task_struct;task_struct 是 Linux 内核的一种数据结构,它会被装载到 RAM(内存) 里并且包含着进程的信息。
3. 组织进程
那么操作系统是如何管理进程组织进程的呢?首先,磁盘上的可执行程序通过加载、拷贝到内存中,因为操作系统是最先被加载到内存中的,所以操作系统会对相应的可执行程序创建相应的 PCB ,其中这个 PCB 中包含这个进程的所有属性;
对于每个进程,操作系统都会对其创建相应的 PCB,再对这些 PCB 进行组织,而操作系统对它们的组织形式是将它们像链表一样连接起来管理,那么对进程的管理就被建模成了对 PCB 的管理,也就是对链表的增删查改;如下图所示:
那么我们平时所听说的进程在排队,实际上就是 PCB 在某个队列中在排队,例如有一个运行队列:
所以准确地来说,进程 = 可执行程序 + 内核数据结构(PCB);其中 PCB 是方便操作系统对进程进行管理的。
除此之外,一个进程不仅仅只会在某个指定的队列或者链表中,它可能会出现在多个队列或者链表当中。
实际上,Linux 中定义 task_struct 结构体的时候,首先先定义一个双链表的结构体:
struct dlist { struct dlist* next; struct dlist* prev; }
利用这个双链表的结构指针直接指向下一个 task_struct 的某个属性队列或者链表中,如下图:
所以这个 task_struct 的结构体应该是这样设计的:
struct task_struct { dlist list; // 系统所有进程所在的队列 dlist queue; // 同时这个进程还可以在队列中 // 也可以在其它各种结构中 }
4. 查看进程
(1)通过系统调用接口查看
查看所有进程的指令为:ps axj
.
例如我们先随便编写一个程序,随后运行起来:
此时我们需要找到这个进程并查看这个进程,但是直接使用 ps axj
信息量太过大,我们需要过滤一些信息,如:
ps axj | head -1 # 只显示第一行的数据,即头标 ps axj | grep xxx # 查找名为 xxx 的字符串
所以我们可以将上面两句拼在一起,即可找到我们相应的进程并且很简洁,其指令为:ps axj | head -1 && ps axj | grep mytest
:
其中,我们查看 mytest 的进程,为什么会有图中的 grep 进程呢?
因为我们在执行 grep 的时候,它也变成了一个进程,所以我们查看过滤的时候没有把它过滤掉。
其中我们可以看到,图中有 PID 以及 PPID 的头标,这两个分别是这个进程的 进程id 和 父进程id.
我们还可以通过让进程自己打印自己的 pid / ppid,使用到的系统接口是 getpid() / getppid
,其中它们包含的头文件如下:
返回值是返回其 pid :
我们可以写一个程序验证一下:
随后我们使用 ps 指令查看 其 pid:
如上面两个图所示,两种方法都可以查看进程的 pid / ppid.
当我们想要终止掉一个进程的时候,我们可以使用指令:kill -9 +pid
.
但是我们可以多次执行和结束一个进程,观察它的 pid 和 ppid,如下图:
我们可以观察到,这个进程每次的 pid 都是不一样的,但是它的 ppid 却是一样的,这是为什么呢?我们可以查看一下它的 ppid:
我们可以看到,这个居然是 bash,也就是 Linux 中的命令行解释器,所以我们得出一个结论,我们命令行启动的进程,都是 bash 的子进程。
(2)通过 /proc 系统文件夹查看
查看进程信息的第二种方式是通过系统文件夹查看,但是我们常用的是第一种方式查看,这个需要了解一下。
我们可以通过指令 ls /proc/
查看系统文件夹:
后面跟上我们需要查找的 pid 即可找到,如:
加上 -dl
选项是只查看这个进程的文件夹和它的属性。
我们只加上 -l
选项,可以看到这个进程的许多信息属性,其中有两个需要我们了解,那就是 cwd 和 exe:
其中,exe 是该进程对应的可执行程序,我们可以看到它后面跟的是它所在的路径,所以说明一个进程是能够找到自己的可执行程序的;
其次,cwd 是当前的工作目录,假设我们当前这个进程有文件操作相关的代码,需要创建一个文件,那么这个文件就会被创建在这个当前的工作目录下;默认情况下,进程启动所处的路径,就是当前路径。当前工作目录是可以修改的。
5. 通过系统调用创建进程 - fork
(1)初识 fork
fork 是通过代码创建进程,是一种系统调用。
我们先使用 man fork
初步认识一下 fork:
我们可以看到, fork() 是创建一个子进程。我们继续看它的返回值:
我们可以看到,如果成功,会将子进程的 pid 返回给父进程,会将 0 返回给子进程。
下面我们简单看一下 fork() 的用法;我们在原来的代码上稍作修改:
#include <stdio.h> #include <unistd.h> #include <sys/types.h> int main() { printf("我是一个父进程, 我的pid:%d, ppid:%d\n", getpid(), getppid()); pid_t id = fork(); // fork之后,用if进行分流 if(id < 0) return 1; // child else if(id == 0) { while(1) { printf("我是一个进程, pid:%d, ppid:%d, ret:%d, 我正在下载任务\n", getpid(), getppid(), id); sleep(1); } } // parent else { while(1) { printf("我是一个进程, pid:%d, ppid:%d, ret:%d, 我正在播放任务\n", getpid(), getppid(), id); sleep(1); printf("\n"); } } }
我们观察代码运行的结果:
我们可以观察到,父进程的 pid 是 15264;ppid 是28553,也就是 bash;返回值是其子进程的 pid;
子进程的 pid 是 15265;ppid 是 15264;返回值是 0.
结合目前,我们得出一个小结论,只有父进程执行 fork() 之前的代码,fork() 之后的代码,父子进程都要执行。
那么我们为什么要使用 fork() 创建子进程呢?原因是我们想让子进程协助父进程完成一些任务,这些工作是单线程解决不了的,比如上面的代码中,父进程执行播放任务,子进程执行下载任务。
(2)fork 原理
我们见识了 fork() 的用法之后,不禁会有很多问题,比如 fork() 为什么会有两个返回值?为何同一个变量会有不同的值?等等。
- fork() 在干什么?
首先我们先了解一下 fork() 在做什么,fork() 创建子进程,系统中会多一个子进程,os 会以父进程为模板,为子进程创建一个 PCB,而父进程会与子进程共享代码和数据,所以 fork() 之后,父子进程会执行一样的代码。 - fork() 之后,父子进程谁先运行?
创建完成子进程后,只是一个开始,接下来,系统的其它进程,父进程,子进程都要被调度执行;当父子进程的 PCB 都被创建并在运行队列中排队时,那一个进程的 PCB 先被选择调度,哪个进程就先运行;而谁先被调度我们不能确定,是由操作系统决定。 - 为什么 fork() 的两个返回值,会给父进程返回子进程的 pid,给子进程返回 0?
因为子进程的 pid 具有唯一性,是为了方便父进程对不同的子进程进行区分管理;而给子进程返回 0 是因为子进程的父进程也是有唯一性,它们都是同一个父进程的,对于子进程而言,只需要知道是否成功即可。 - 为什么 fork() 会有两个返回值?
我们首先需要知道,如果一个函数执行到 return,它的核心工作已经完成了,但是 fork() 之后,代码共享,当代码执行到 return 的时候,return 也是代码,是代码就要进行共享,所以当父进程被调度时,就要执行 return;当子进程被调度时,也要执行 return. - 如何理解同一个变量会有不同的值?
假设我们启动一个qq,启动微信,启动浏览器,这些都是进程,终止qq或者微信的进程,浏览器的进程还在吗?答案是肯定在的;如果是对于父子进程来说,父进程被终止,子进程还在吗?或者反过来呢?答案是在的!所以我们得出一个结论,进程之间运行的时候,是具有独立性的,无论是什么关系!进程的独立性,首先是表现在各自的PCB;进程之间不会相互影响,就算父子进程共享代码,代码也只是只读的,不会影响;但是数据父子是会修改的,所以代码共享时,数据各个进程必须想办法私有一份,目前我们只需要知道,它是采用了写时拷贝的方式即可,当父进程或子进程对数据进行修改时,操作系统会对这份数据进行拷贝。所以,当 fork 的子进程执行到 return 的时候,这个值需要返回给变量 id,返回的本质也是写入,而 id 也是父进程定义的变量,保存的是数据,所以返回的时候,发生了写时拷贝,所以同一个变量会有不同的值。
四、进程状态
进程状态,说白了就是 PCB 中的一个字段,就是 PCB 中的一个变量,假设这个变量为 status,进程状态变化的本质就是:1、更改 PCB 中 status 变量;2、将 PCB 连入不同的队列中;我们所说的所有进程,都只和进程的 PCB 有关, 和进程的代码数据无关。
进程的主要几种状态的转换图如下:
下面我们具体分析各种状态,并延申出其它状态。
1. 运行状态
运行状态就是可以随时被调度的状态,上图中的创建、就绪、执行其实都属于运行状态,只要在运行队列中的进程,状态都是运行状态。
2. 阻塞状态
我们的代码中,一定或多或少会访问系统中的某些资源,比如磁盘、键盘、网卡等各种硬件设备。
例如我们的代码中出现 scanf()、cin 等,本质是我们从键盘中读取数据,如果我们就是不输入,键盘上面的数据就是没有就绪,也就是说,我们的进程要访问的资源没有就绪,它也就不具备访问条件,该进程的代码也就无法继续向后执行,该进程就是阻塞状态。
当一个进程阻塞了,我们应该看到的现象应该是:
- 进程卡住了;
- PCB 没有在运行队列中,并且状态不是 R(running) ,CPU 就不会调度我们的进程了;
这个时候我们的计算就比较卡了。
3. 挂起状态
如果一个进程当前被阻塞了,注定了这个进程在它所等待的资源没有就绪的时候,该进程是无法被调度的,如果此时,恰好操作系统内的内存资源已经严重不足了,OS 针对所有阻塞进程,会将内存数据进行置换到外设;此时我们就不必担心慢的问题了,因为这个是必然的,主要是让 OS 继续执行下去。(OS 会将数据置换到 swap 分区。)被置换后的数据所在的进程此时就是挂起状态。
swap 分区 一般不能太大也不能太小,太小的话不够用;太大会造成 cpu 过度依赖 swap分区。
当进程被 OS 调度,被置换出去的进程代码和数据,又会重新被加载进来。
4. Linux 中进程的具体状态
一个进程可以有几个状态(在 Linux 内核里,进程有时候也叫做任务)。下面的状态在 kernel 源代码里定义:
static const char * const task_state_array[] = { "R (running)", /* 0 */ "S (sleeping)", /* 1 */ "D (disk sleep)", /* 2 */ "T (stopped)", /* 4 */ "t (tracing stop)", /* 8 */ "X (dead)", /* 16 */ "Z (zombie)", /* 32 */ };
(1)R运行状态
R运行状态(running):并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
此处的R运行状态就是我们上面所学的运行状态。
我们运行一个程序,然后查看它的进程状态,如下,程序已运行:
查看它的运行状态:
如上图,我们看到 STAT 这一列,就是状态的意思,那么这里的状态为什么是 S+ 呢?
首先,我们的程序中调用了 printf() 函数,进行了大量了 I/O 操作,cpu 需要访问我们的外设,这里的外设指的是显示屏,此时我们的显示屏不一定处于就绪状态,所以大部分情况下都会是 S 状态(下面会介绍),即阻塞状态;可能运气好的时候这个进程刚好被调度了,就会偶尔有一两次会查到这个进程的状态是 R 状态。如果我们想一直看到 R 状态,可以把程序中的 printf 函数屏蔽掉,即没有大量的 I/O 操作,此时查看到的状态就是 R.
那么为什么 S 后面还有个 + 呢?这里就要了解一下前台进程和后台进程了。所谓的前台进程,就是一旦这种进程启动,我们的命令行 bash 无法继续运行,而且可以直接 ctrl + c 直接终止掉的,这就是前台进程;例如,我们上面的进程就是前台进程,我们无法使用指令:
因为前台进程只能有一个,所以我们无法执行其它指令;可以直接 ctrl + c 终止掉:
前台进程在其进程状态的后面会有一个 + 号;
当然我们也可以使用后台进程的方式启动,我们使用 ./mytest
的方式默认启动的是前台进程;当我们在其后加 & 就是使用后台进程的方式启动了,即 ./mytest &
,例如:
此时我们查看进程状态:
我们发现此时 S 的后面没有 + 号了,这就是后台进程。
(2)阻塞状态
- S睡眠状态(sleeping):意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(浅度睡眠)(interruptible sleep))。浅度睡眠可以被终止,会对外部信号做出响应。Linux 在实在没有办法的时候,会通过终止进程的方式,节省资源,此时 S 状态的进程是可被终止的。
- D磁盘休眠状态(Disk sleep) 也叫不可中断睡眠状态(深度睡眠)(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。这个状态是专门针对磁盘来设计的,而且处于当前进程状态是不可被终止的,操作系统也没有资格!因为访问磁盘的速度很慢,必须要等待磁盘做出响应。但是这个状态我们一般看不见,因为当我们看到这个状态,就说明计算机快要挂掉了。
- T停止状态(stopped):可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。但是为什么要暂停呢?在进程访问软件资源的时候,可能暂时不让进程进行访问,就将进程设置为STOP;
- t停止状态(tracing stop) :debug 程序的时候,追踪程序,遇到断点,进程暂停了。
上面中的 S、D、T、t 状态都可以称为我们所学的阻塞状态。
(3)死亡状态
- X死亡状态(dead):这个状态只是一个返回状态,我们不会在任务列表里看到这个状态。
我们创建进程的原因是我们需要完成某种任务,但是如何知道该进程把任务完成得怎么样呢?进程在退出的时候,要有一些退出信息,表明自己把任务完成得怎么样,该信息是由该进程的父进程读取的;这些信息由OS写入到当前退出进程的PCB中,可以允许进程的代码和数据空间被释放,但是不能允许进程的 PCB 被立即释放,因为要让OS或者父进程读取退出进程的PCB中的退出信息,得知子进程退出的原因。
如果一个进程退出了,但是还没有被父进程或者OS读取,OS必须维护这个退出进程的PCB结构,此时这个进程不算退出,这个时候这个进程就处于Z僵尸状态。
- Z(zombie) - 僵尸进程
- 僵尸状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵尸进程。
- 僵尸进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
- 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。
如果一个进程进入Z状态了,但是父进程就是不回收它,它的PCB就会一直存在,就有可能会造成内存泄漏!
当它被父进程或者OS读取之后,PCB 状态先被改成X状态,然后才会被完全释放。
我们模拟一下创建一个僵尸进程:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 5 int main() 6 { 7 pid_t id = fork(); 8 if(id < 0) return 1; 9 else if(id == 0) // 子进程 10 { 11 int cnt = 3; 12 while(cnt--) 13 { 14 printf("i am a child, run times: %d\n", cnt); 15 sleep(1); 16 } 17 printf("i am a zombie now\n"); 18 exit(-1); 19 } 20 else // 父进程 21 { 22 while(1) 23 { 24 printf("i am a father, running any times!\n"); 25 sleep(1); 26 } 27 } 28 29 return 0; 30 }
如以上代码,我们使用 fork 让两个进程分流,子进程执行3次后退出,父进程一直运行,不退出,此时我们查看两个进程的状态:
此时我们看到,子进程进入了Z僵尸状态。
至此值得关注的进程状态全部讲解完成,下面来认识另一种进程:孤儿进程。
- 孤儿进程
- 父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?
- 父进程先退出,子进程就称之为 “孤儿进程”。
- 孤儿进程被1号进程init 进程 或者 systemd 进程领养,当然要由 init进程 或者 systemd 进程 回收。
我们先模拟一下孤儿进程的代码,只需要将上面的僵尸进程的代码中父进程和子进程的代码换过来即可:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 5 int main() 6 { 7 pid_t id = fork(); 8 if(id < 0) return 1; 9 else if(id > 0) // 父进程 10 { 11 int cnt = 3; 12 while(cnt--) 13 { 14 printf("i am a father, run times: %d\n", cnt); 15 sleep(1); 16 } 17 exit(-1); 18 } 19 else // 子进程 20 { 21 while(1) 22 { 23 printf("i am a child, running any times!\n"); 24 sleep(1); 25 } 26 } 27 28 return 0; 29 }
运行起来后,我们查看两个进程的状态:
过了一会后我们再查看:
此时只剩下子进程了,而且它的 ppid 变成了 1,也就是它被 1 号进程领养了,1 号进程其实就是 init / system 进程,也就是操作系统。
但是在父进程退出之前,没有变成Z状态,而是直接没了,这是为什么呢?原因是原来的父进程也有自己的父进程!那就是 bash,在父进程退出时,会直接被 bash 回收释放;而留下的子进程则成为了孤儿进程,它会被 1 号进程领养。
五、进程优先级
(1) PRI & NI
我们已经知道,进程可能会在某个队列中排队,而排队的本质就是在确认优先级。那么什么是优先级呢?就是进程为了得到cpu的调度的先后顺序。出现优先级的本质原因是因为cpu资源不足,一个操作系统一般只有一个cpu,而这个cpu需要调度的进程太多了,所以才出现优先级这一说法。
确认优先级的做法其实就是修改PCB中的一个int字段,其中这个字段是PRI,其数值越小,优先级越大。我们可以通过指令 ps -la
来查看进程的优先级:
其中 PRI 这一列就是进程的优先级;Linux 进程的优先级数值范围是 60~99,其中默认进程的优先级都是 80.
但是 Linux 是支持动态优先级调整的,Linux 进程 PCB 中存在一个nice值,进程优先级的修正数据,其中:PRI(新) = PRI(旧) + nice,其中 PRI(旧) 每次修改都是从 80 开始!
我们可以使用指令 top
修改已存在进程的 nice 值,但是由于权限问题某些进程我们修改不了,我们可以使用 sudo top
修改。修改步骤,先使用 top
进入修改模式,然后按下 r
,输入进程 pid,回车再输入 nice 值即可完成修改,例如:
我们尝试修改某个进程的优先级:
注意 nice 调整的最小值是 -20,超过部分统一当成 -20;最大值为 19,超过部分 统一当成 19. 为什么要这样规定呢?因为 OS 在调度的时候,会较为均衡的让每一个进程都得到调度,如果没有限定优先级的范围,容易导致优先级较低的进程,长时间得不到cpu资源,这种情况叫做进程饥饿。
(2)其他概念
- 竞争性:系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级。
- 独立性:多进程运行,需要独享各种资源,多进程运行期间互不干扰。
- 并行:多个进程在多个CPU下分别,同时进行运行,这称之为并行。
- 并发:多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。
每一个进程并不是占有CPU就一直运行,每隔一段时间,会自动被从CPU上剥离下来,这段时间称为时间片;但是Linux内核中不仅仅只有时间片,因为只有时间片的话会显得太呆板,假设每个进程都运行1ms就下来,那么每一个平均下来都是这样子的。所以Linux内核支持进程之间进行cpu资源抢占的,基于时间片的轮转式抢占式内核!简单来说,就是当一个优先级为80的进程运行了0.5ms,此时来了一个优先级更高的进程,此时就会强行将优先级为80的进程剥离下来,把优先级更高的放上去。
(3)进程间切换
我们的程序/进程中,怎么知道我们当前运行到哪里呢?或者我们上次运行到哪呢?如何做到进程间跳转?假如我们有一个10000行代码的程序,在时间片内运行了1000行代码,然后进行进程切换,那么当下一次又到这个进程调度的时候,cpu怎么知道我上一次运行到哪里呢?
在 cpu 内,有很多的寄存器,其中有一个叫做 eip (pc指针)的程序计数器,它会保存当前正在运行的进程的指令的下一条指令的地址,换言之,这条指令的地址就能知道我们下一条应该执行哪条语句,这样就能做到在函数间/进程间跳转了。
我们的进程在运行的时候,是会使用这些寄存器的,我们的进程,会产生各种数据,这些数据都会在寄存器中临时保存。如果我们有多个进程,各个进程在CPU寄存器中形成的临时数据,都应该是不一样的,这叫做进程的硬件上下文。所以,CPU寄存器硬件只有一套,而10个进程的上下文数据应该有10套,因为寄存器不等于寄存器中的内容!
所以我们要明白,当进程间进行切换的时候cpu首先要保存cpu寄存器中的内容,再把切换的进程放上来,不然的话就会相互影响了。那么寄存器中的内容保存在哪里呢?答案是保存在对应进程的PCB中,本质就是CPU寄存器的内容保存到内存中!当cpu中寄存器的内容保存到PCB中后,寄存器中原来的内容也不会清空,而是等下一个进程上来直接使用,即会将原来的数据进行覆盖。