文章目录
冯诺依曼体系结构
冯诺依曼体系结构(Von Neumann Architecture)是计算机的基本设计理念之一,由美国数学家约翰·冯·诺依曼于1945年提出,也被称为"冯诺依曼模型"或"冯诺依曼计算机体系结构"。它的核心思想是将程序和数据存储在计算机的内存中,并通过中央处理单元(CPU)执行程序。冯诺依曼体系结构至今仍然是大多数计算机的基础架构。
-
中央处理器(
CPU
):-
控制单元(
CU
):负责指挥计算机各部分的工作。 -
算术逻辑单元(
ALU
):进行算术和逻辑运算。 -
寄存器:用于暂时存储数据和指令。
-
-
内存(
RAM
):- 存储程序和数据。冯诺依曼结构中的程序和数据都存储在同一内存中。
-
输入设备:用于向计算机输入数据,例如键盘、鼠标等。
-
输出设备:用于输出处理结果,例如显示器、打印机等。
-
总线:用于在各个组件之间传输数据和指令的通道。
注意:
-
上面的存储器指的就是内存
-
不考虑缓存的情况下这里的CPU只能对内存中的数据进行操作,不能从外设 (输入和输出设备)中获取数据
-
外设(输入或输出设备)要输入或输出数据,只能从内存中获取
-
总的来说,所有设备都只能与内存打交道
为什么体系结构中要存在内存?
CPU处理速度非常快,但是输入数据的速度相较于CPU的速度是非常慢的,这就导致了很多时候CPU都在等待数据的输入,严重浪费了CPU的性能,所以增加内存,让CPU直接跟内存交换数据,充分发挥CPU的性能。(内存输入输出的数据的速度是非常快的)
计算机存储金字塔:
冯诺依曼瓶颈:
冯诺依曼架构存在一个著名的问题,即"冯诺依曼瓶颈"(Von Neumann Bottleneck)。这是由于程序和数据共享同一个内存系统,CPU在执行指令时需要频繁地从内存读取指令和数据,导致内存的读写速度成为限制计算机性能的瓶颈。随着计算机硬件的不断发展,解决冯诺依曼瓶颈的问题成为计算机体系结构研究的一个重要方向。
总的来说,冯诺依曼体系结构让计算机保持一定处理速度的同时,降低了计算机的成本,使得计算机能够进入各家各户,为之后互联网的发展奠定了基础。
操作系统
操作系统(Operating System,简称OS)是管理计算机硬件与软件资源的系统软件,它为应用程序提供了一个运行环境,并为用户提供与计算机硬件交互的接口。
操作系统包括:
-
内核(进程管理,内存管理,文件管理,驱动管理)
-
其他程序(例如函数库,shell程序等等)
一般而言,操作系统指的是内核。
设计目的:
-
操作系统对下与硬件交互,进行软硬资源的管理(手段)
-
操作系统对上为用户程序(应用程序)提供⼀个良好的执行环境(目的)
-
软硬件体系结构是层状结构
-
访问操作系统,其实就是系统调用(系统提供的函数)
-
只要程序运行访问了硬件,那么必须贯穿整个软硬件体系结构
-
函数库在底层封装了系统调用
系统调用与库函数:
操作系统会暴露部分接口供上层开发者使用,这部分接口就是系统调用。
系统调用的功能比较基础,对使用者要求较高,所以一部分开发者将系统调用的接口进行封装,从而形成了库,有利于开发者进行二次开发。
进程
进程概念
进程 (Process)是计算机中正在执行的程序的一个实例。它是操作系统资源分配和调度的基本单位,是操作系统管理计算机硬件和软件资源时的核心概念之一。
程序与进程的区别:
-
程序:是一组静态的指令集合(二进制文件),它是一个静态的实体,不会占用计算机的系统资源。
-
进程:是程序的执行实例,它是动态的,并且在执行过程中会占用系统资源(如CPU时间、内存等)。
内存在同一时间会有成百上千的被加载进来的程序,操作系统是需要对其进行管理的。
操作系统会给每个代码和数据块建立一个struct
结构体(进程控制块),结构体存的是对代码和数据块的信息,也有相应的指针指向对应的代码和数据。许多这样的结构体组成双链表,也就是进程列表。
进程控制块PCB→Process Control Block
Linux系统中PCB
是task_struct
PCB相关内容: https://www.cnblogs.com/tongyan2/p/5544887.html
总结:进程 = PCB(task_struct) + 对应的代码和数据
进程信息被放在进程控制块中,可以理解为进程属性的集合。操作系统要对进程进行管理,其实就是对描述进程的task_struct形成的数据结构进行增删查改。
注:我们在Linux执行的指令、工具、程序,运行起来都是进程
查看进程的方法
c
getpid() //获取进程pid
getppid() //获取父进程pid
pid
就是进程的标识符(编号);- 这两个函数都是系统调用
我们可以使用 man 2 getpid
来查看相关信息。
其返回值就是一个整型变量,成功调用返回一个大于0的整数,失败就返回-1。
bash
ps axj | head -1 ; ps axj | grep mypro //查看mypro进程相关信息
由于grep
本来也是个进程,查找mypro
进程,grep
进程就会带上mypro
的关键字,那么查出来的进程也会有grep
进程,要屏蔽的话就使用grep -v
反向匹配
bash
ps axj | head -1 ; ps axj | grep mypro | grep -v grep
bash
ls /proc
ls /proc
以文件的方式查看进程,proc
目录记录进程信息,每个数字目录代表一个正在运行的进程,进程结束后,对应的目录文件就会删除。
我们先运行一个pid
为70816
的进程。
再使用ls /proc
查看进程信息,可以看到有70816
的文件夹。
让我们直接查看70816
的文件夹。
-
exe
代表我们的可执行文件,如果将其删除不会对正在运行的进程造成影响,因为删除的是磁盘上的程序,可执行文件已经被加载在内存里了。 -
cmd
(current work directory
)是进程所在的当前文件路径,我们的程序在创建文件时默认是在当前路径下创建的,而当前路径就是通过cmd
获取的。
我们可以使用chdir
改变当前进程的文件路径,以下为相关信息。
结束进程:ctrl+c
或 kill -9 pid
父进程pid不变现象:
多次启动进程时,会发现其父进程的pid不变,这是因为该父进程其实就是bash
进程 ,我们执行的程序或者指令大多都是bash
的子进程。
当子进程出问题,不会影响bash
进行,因为进程具有独立性;当我们启动xshell
时,系统自动生成bash
进程。
创建进程
创建进程需要使用系统调用fork
函数。
cpp
#include <unistd.h>
pid_t fork();
-
fork
系统调用,没有参数,有两个返回值 -
fork
在创建进程成功时,给父进程返回子进程的pid,给子进程返回0,失败时返回-1给父进程(没有子进程创建)。这样做的原因是因为父进程与子进程的关系是一对多的关系,将子进程的pid返回给父进程让其可以区分不同的子进程。 -
父子进程代码共享,数据各自开辟空间,私有一份(写时拷贝)
fork的使用示例:
C
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id == 0)
{
//子进程
while(1)
{
sleep(1);
printf("I am a child process, my pid: %d\n", getpid());
printf("\n");
}
}
else
{
//父进程
while(1)
{
sleep(1);
printf("I am a fathermak process, my pid: %d\n", getpid());
}
}
return 0;
}
从上面的现象我们可以看出,id == 0 和 id > 0 同时成立,这是因为当我们创建完子进程后,其实已经存在了两个进程,会共用fork之后的代码,对于父进程 id > 0,对于子进程 id == 0 ,在同一时间内两个进程执行不同的代码,就会产生上图的结果。
为什么fork
会返回两个值?
在fork函数内部,执行到最后的return语句时,子进程已经创建好了,两个进程就会同时执行return语句,fork就能返回两个值,我们以前认知的一个函数只能有一个返回值是在同一个进程的条件下才成立的。
为什么 id == 0
和 id > 0
同时成立?
进程具有独立性。父子进程不同的PCB、代码虽然共享,但是只读的,不会影响独立性
父进程和子进程共用代码和数据,当任意进程尝试修改数据,操作系统在数据被修改前拷贝一份,让目标进程修改这份数据的拷贝。通过这种写时拷贝技术让不同进程的数据独立,从而保证进程的独立性。
进程状态
进程状态说明了进程当前的行为
进程状态就是task_struct
中的一个整数变量
各种进程状态转换图
一个进程会有如下的很多状态
运行 && 阻塞 && 挂起
CPU能执行的进程只有一个,但是进程会有很多个,所以这些进程就需要排队等待执行,而这个队列就是调度队列。一个CPU只能有一个调度队列。
以下为基本调度结构:
注:task_struct
既可以属于全局中的PCB
双链表,也可以属于调度队列中的队列,跟以前一个节点只属于一个数据结构稍有不同。
调度算法之一:FIFO
,先进先出。CPU
依次选择一个task_struct
来执行。
进程的运行阻塞挂起状态
运行 :但是只要一个进程在调度队列中,它就是running
状态。换句话说,进程是运行状态,要么它持有CPU
,要么就是完全准备好随时被调度。
阻塞 :进程等待某种设备或者资源就绪。如:C语言的scanf
C++的cin
,这些设备可以是键盘、显示器、网卡、磁盘、摄像头、话筒等
运行和阻塞的切换
操作系统中不仅有调度队列,也有其他的队列,比如设备队列。我们举scnaf
例子来说明进程运行与阻塞状态的改变。
一个进程要
scanf
, 操作系统要去等待键盘输入,进程无法执行,将该进程从调度队列中拿掉,链接到其他队列(特定设备的等待队列 ),该进程不会被调度,此时进程就处于阻塞状态,当键盘输入数据完毕, 进程又会链接到调度队列,进程状态变为运行状态
进程状态的变化、表现之一,就是在不同的队列中进行流动,本质都是数据结构的增删查改。
挂起状态
-
阻塞挂起 :当内存资源不足时,操作系统就会将阻塞进程对应的代码和数据送到磁盘的
swap
分区以腾出内存空间资源,此时进程变为阻塞挂起状态。 -
运行挂起 :如果内存资源严重不足,操作系统就会将调度队列末端的
PCB
对应的代码和数据交换到磁盘的swap
分区,此时进程变为运行挂起状态。
对于进程列表(PCB双链表)的理解
一个PCB是如何做到属于进程列表(全局双链表)又属于调度队列或者其他队列?
双链表一般定义,结构体里定义当前结构体的指针。
C
struct Node
{
int data;
struct Node* next;
struct Node* prev;
}
在task_struct
中,将next
指针和prev
指针再封装成list_head
的结构体作为task_struct
的成员变量。
C
struct list_head
{
struct list_head* next;
struct list_head* prev;
}
struct task_struct
{
......
list_head links;
}
list_head
通过偏移量去找到task_struct
的其他成员,或者说去构建一个task_struct
的指针。
C
&((struct task_stuct*)0→links) //links在`ask_struct的统一偏移量
(struct task_struct*)(next/list - 偏移量) //构建出一个`task_struct`的指针
我们可以通过构建出来的指针去访问task_struct
的其他成员。而一个task_struct
里会有多个list_head
类型的成员,通过各个list_head
类型的成员相互连接task_struct
,这样就做到了一个task_struct
既属于进程列表又属于调度队列或者其他队列
Linux中具体的进程状态
在Linux内核源码中,task_state_array
指针数组用于存放进程的各种状态。
C
static const char* const task_state_array[] = {
"R(running)",
"S(sleeping)",
"D(disk sleeping)",
"T(stopped)",
"t(tracing stop)",
"x(dead)",
"Z(zombie)"
}
- R:运行状态
R+ 代表进程是运行状态,+代表是前台运行,没有就是后台运行
-
S:阻塞状态(Linux叫休眠),浅度休眠,可中断休眠,可以直接kill 进程
-
D :也属于阻塞状态,深度休眠(不可中断休眠)disk是磁盘,跟磁盘有关。当进程涉及磁盘进行深度I/O时,进程状态为D深度休眠状态,不能被OS杀掉,防止出现数据丢失的问题(OS在极端情况下(资源严重不足)会杀掉一部分进程,防止OS崩溃)
-
T、t :暂停状态,Linux独有,对于T,OS怀疑进程有问题,进程做了非法操作,OS就将进程暂停(止损操作) ,让用户处理,或者直接对一个不断循环的程序输入ctrl+c ,此时进程也是T暂停;t是追踪暂停,debug代码时,程序在断点停下,进程是t追踪暂停状态
-
X:死亡状态,进程完全结束了(释放对应的代码和PCB)就是死亡状态。
-
Z:僵尸进程
一个子进程结束后了,不能立刻释放所有的资源,其对应的代码可以被释放掉,但是PCB不能被释放,因为父进程要获取子进程的PCB信息,或者说父进程需要子进程是否完成父进程交代的任务的结果。 在子进程结束后,父进程获取子进程的PCB信息之前,子进程是僵尸状态。
内存泄漏问题
-
进程结束了,或者说程序结束了,内存泄漏问题就不在了,因为OS会回收动态分配的空间。
-
僵尸进程的内存泄漏问题是很严重的(系统界别)
-
常驻内存的进程是非常惧怕内存泄漏问题的。
孤儿进程
如果在父子进程的关系中,父进程先于子进程结束,子进程就会变成孤儿 进程。孤儿进程会被1号进程(操作系统)领养。
孤儿进程如果不被领养则会导致内存泄漏的问题。
进程变成孤儿 进程会变成后台进程,一般的ctrl+c不能结束孤儿 进程,结束孤儿 kill -9 pid
指令。
1号进程
进程优先级
进程优先级指的是进程获取CPU资源的先后顺序,就好比学生在食堂排队打饭。优先级高的进程有优先占用CPU执行的权利。
- 进程优先级与权限 :优先级决定的是获取资源的顺序,权限决定的是能不能获取资源。
进程优先级的存在可以让CPU有序的执行各个进程,发挥CPU的性能优势。
进程优先级是一种数字,其值越低,优先级越高。
注:现在大多数的操作系统是基于时间片的操作系统,考虑公平性,优先级可能会变化,但是幅度不会太大。
UID
(users id
),用于标识用户的特定数字,让操作系统可以区分拥有者、所属组和Other
-
衡量进程优先级的具体标准进程优先级
PRI
,默认值为80;进程优先级的修正数据NI
,默认值为0; -
PRI(new) = PRI(default) + nice
-
NI
的范围是[-20, 19],由此推出进程优先级的范围[60, 99]。
进程级先级的设立要合理,不然可能会导致低优先级的进程得不到CPU
的使用权,导致进程饥饿。
进程优先级的设置
进程优先级可以随意改低,但是如果要提高进程优先级,也就是将NI
值改小,需要sudo
提权。
-
top
指令- 终端上输入
top
进入任务管理器 - 输入
r
,然后输入你要修改进程的pid
和修改的NI
值
- 终端上输入
-
nice
指令,修改未运行的进程优先级nice -n <nice_value> <command>
nice_value
是你希望的NI
值command
是你要执行的指令或程序
-
renice
指令,既可以修改未运行的进程的优先级,也可以修改已经运行的进程的优先级renice NI [[-p] pid ...] [[-g] pgrp ...] [[-u] user ...]
NI
:修改后的NI值-p pid
:需要修改优先级的目标进程pid
-g pgrp
:需要修改优先级的目标进程组的id
-u user
:指定进程拥有者为user
的进程来修改优先级
竞争、独立、并行、并发
-
竞争性:系统进程数⽬众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为 了高效完成任务,更合理竞争相关资源,便具有了优先级
-
独立性:多进程运行,需要独享各种资源,多进程运行期间互不干扰
-
并行:多个进程在多个CPU下分别,同时运行,这称之为并行
-
并发:多个进程在⼀个CPU下采用进程切换的⽅式,在⼀段时间之内,让多个进程都得以推进,称之为并发。
进程切换
死循环程序的运行
进程占有CPU,不会把代码执行完,操作系统会分配时间片,只让该进程执行特定的时间,CPU就会切换到另一个进程执行,同样执行特定的时间,经过排队重新执行死循环进程。
死循环进程不会一直占有
CPU
,与其他进程轮流执行的。
CPU&&寄存器
CPU
(中央处理单元)是计算机的核心部件,负责执行程序中的指令并进行数据处理。寄存器是CPU
内部的一个小型高速存储单元,用于存储临时数据、指令和操作数。
进程在使用CPU
时,寄存器保留的是进程的上下文数据。
具体的进程切换
进程在切换时,首先被进程从CPU
上剥离下来,然后将进程的上下文数据,既CPU寄存器的内容保存在task_struct
或TSS
任务状态段,如果重新执行进程,需要恢复进程的上下文数据。
进程切换的核心就是保留和恢复当前进程的硬件上下文数据,既CPU内寄存器的内容。
进程的调度算法
参考:玩转Linux内核进程调度,这一篇就够(所有的知识点)
Linux2.6内核中进程队列的数据结构
-
queue[140]
是一个队列数组,0-99号队列是实时进程,这类进程优先被调度,不在讨论范围内。(队列数组本质是开散列的哈希表) -
当进程被建时,通过其优先级和哈希函数得到该进程在活跃队列数组的位置,将其头插在对应队列。
-
活跃队列数组和过期队列数组的存在,可以防止进程饥饿的出现,如果没有过期队列,优先级较高的进程可能会一直占用
CPU
资源,导致优先级较低的进程无法执行。 -
当活跃队列数组的进程执行完一个时间片的操作,该进程就会被转移到过期队列数组中,当所有活跃队列数组的所有进程都执行完一个时间片的操作,操作系统就会将
active
指针和expired
指针交换。 -
很多操作系统支持内核抢占,优先级较高的新进程就会插入活跃队列,类似于插队行为。
-
位图的存在是为了快速找出优先级高的进程,避免遍历哈希表,从而达成近似于
O(1)
的查找进程。
总的来说,这样的进程队列数据结构实现O(1)的调度算法,可以更大幅度地利用CPU资源,提高整体效率。
环境变量
基本概念
环境变量(environmentvariables)⼀般是指在操作系统中用来指定操作系统运行环境的⼀些参数
如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,⽣成可执⾏程序,原因就是有相关环境变量帮助编译器进行查找。
环境变量通常具有某些特殊⽤途,还有在系统当中通常具有全局特性
main
的命令行参数是程序实现不同功能的方法,是指令选项的实现原理。
C
int main(int argc, char* argv[])
{
for(int i = 0; i < argc; ++i)
printf("argv[%d]: %s\n", i, argv[i]);
return 0;
}
我们在终端输入的指令和选项时,bash会以空格为标识符来划分各个字符串,从而存入main
参数的指针数组,在main
内部根据不同的选项来实现不同的功能。
进程拥有一张argv
表,用于实现选项功能。
二进制程序与指令
我们所写的二进制程序与系统的指令没有本质区别。而执行自定义程序需要指明路径,执行指令却不需要,原因在于系统存在环境变量保留有默认路径。
Linux存在环境变量PATH
,用于标识系统指令的默认搜索路径。
Shell
env #用于查询环境变量
echo $PATH #查询PATH环境变量
环境变量是内存级变量,bash启动时会读取环境变量形成一张环境变量表。
我们输入指令时,bash将其构建成命令行参数表 , 将其解析后在环境变量表搜索路径,在路径后拼上你的指令名,然后创建子进程执行指令。
环境变量是从系统的配置文件来的,bash在启动时就会读取配置文件形成环境变量表。
更多环境变量
HOME
:当前用户的家目录路径
USER
: 当前用户
LOGNAME
:登录名
su -
就会切换当前用户和登录名,相当于重新登录。
HISTSIZE
:记录最近输入指令的条数
PWD
:当前工作目录
OLDPWD
:上一次工作目录
cd -
指令可以在新旧工作目录来回切换
获取环境变量
Shell
export MYENV=xxxx # 导入环境变量
unset MYENV # 取消环境变量
main
的参数最多有三个,是父进程bash传递的
C
int main(int argc, char* argv[], char* env[])
方法一:获取父进程(bash)的环境变量
C
#include <stdio.h>
int main(int argc, char* argv[], char* env[])
{
for(int i = 0; env[i]; ++i)
printf("env[%d]-> %s\n", i, env[i]);
return 0;
}
环境变量是可以被子进程继承的;环境变量具有全局特性。
方法二:
C
getenv("xxx") //获取指定环境变量
方法三:
在 Linux , environ
是一个环境变量的数组,包含了当前进程的环境变量。
bash
environ - user environment
具体信息
使用案例
C
#include <stdio.h>
#include <unistd.h>
extern char** environ;
int main(int argc, char* argv[], char* env[])
{
for (size_t i = 0; environ[i]; i++)
printf("environ[%ld]-> %s\n", i, environ[i]);
return 0;
}
环境变量的特性
环境变量最重要的一个特性就是全局性,可以被被各个进程使用。
补充知识:
- 在命令行也可以创建本地变量,本地变量只属于当前进程,不具有全局性
- 环境变量存在于bash进程。
export
命令是内建命令(built-in command),bash不创建子进程而由bash亲自来执行。export
的作用是导入环境变量,如果是创建子进程来执行,由于进程的独立性,子进程是不能向父进程传输数据的,export
就无法导入环境变量
程序地址空间
同一地址空间下的不同变量
程序地址空间,严格来说应该是进程地址空间(虚拟地址空间),一个系统级别的概念,不是语言层的概念。它不是实际物理内存。
-
共享区:一段镂空的空间
-
静态区:
static
变量其实就是全局变量,只不过受到了main
的限制,只能在main
里面访问。 -
只读区:存放代码、常量字符串
-
栈区:用于临时存储函数调用信息、局部变量和控制信息的一个区域
-
堆区:用于动态分配的内存区域
证明内存地址是虚拟地址。
C
#include <stdio.h>
#include <unistd.h>
int gval = 100;
int main()
{
__pid_t id = fork();
if(id == 0)
{
while(1)
{
printf("子进程: gval: %d, &gval: %p, pid: %d\n", gval, &gval, getpid());
gval++;
sleep(1);
}
}
else
{
while(1)
{
printf("父进程: gval: %d, &gval: %p\n", gval, &gval);
sleep(1);
}
}
return 0;
}
同一地址空间下的变量不可能同时是两个值,只能说明内存地址是虚拟地址。
原因:
-
一个进程对应一个虚拟地址空间,32位机器 -> 2^32个字节 -> 4GB
-
一个进程,一套页表,页表是用于虚拟地址和物理地址做相对映射的。
-
页表相当于一个哈希表,存储的是虚拟地址-物理地址。子进程会继承父进程的页表,所以父子进程共用数据和代码,当任意一方进程尝试修改变量时,操作系统会将变量复制到另外的物理地址空间上,其页表的物理地址做出相应修改,然后让目标进程修改变量。所以会出现同一地址空间下的不同变量(虚拟地址是一样的,物理地址不一样)
注:用户无法查看实际的物理地址。
虚拟地址空间与进程地址空间
操作系统管理进程地址空间采取画饼时管理,让每一个进程都认为自己独占所有物理内存。
虚拟地址空间也是一个结构体,在Linux上是
mm_struct
如何在mm_struct
划分栈和堆等空间?
记录区域的开始和结束,调整区域就修改其开始和结束。
区域划分需要确定区域的开始和结束。
c
stuct mm_struct
{
long code_start, code_end;
long init_start, init_end;
//...
}
由于每个不同进程的虚拟内存区域功能和内部机制都不同,因此⼀个进程使⽤多个vm_area_struct
结构来分别表示不同类型的虚拟内存区域(栈、堆、静态区等)。
有关虚拟地址空间结构体更深的认识:
虚拟地址是一个结构体,在开辟空间初始化时,其变量的值从内存加载的代码和数据的大小来,加载程序到内存时会申请物理空间,同时在虚拟地址空间也申请相应大小的空间(调整区域划分)。
为什么会存在虚拟地址空间?
-
有序: 将地址从"无序"变为"有序",从磁盘加载的数据和代码可以在物理内存的任何位置,通过页表的映射可以将这些分离的地址空间变得相对统一,在进程视⻆所有的内存分布都可以是有序的。
-
保护:页表中不仅有虚拟地址-物理地址,还有权限标志位,在对地址进行操作时,会检查其是否合法(是否权限越界),操作系统会停滞或者结束不合法的地址操作(比如访问野指针,对常量字符串进行修改)的进程,保护了物理内存中的所有的合法数据。
-
低耦合: 虚拟地址空间的存在可以让进程管理 和内存管理 进行一定的解耦合,有助于后续的维护操作。
一般而言,在创建进程时先要有内核数据结构,再加载代码和数据,但是也可以不加载代码,只有task_struct、mm_struct;
进程挂起更深刻的认识:
- 进程挂起:将页表的物理地址清空,将内存里的代码和数据换到磁盘的swap分区。
Have a good day😏
See you next time, guys!😁✨🎞