【万字精讲】一篇文章帮你了解代码从运行到结果之间所有过程(一):编写到CPU执行
作为一名计科学生,每天学来学去,有一天突然发现自己还不清楚每天写的代码的运行流程,内心感到羞愧不已,于是下定决心一定要弄懂不会的来,所以不断的找寻相关知识,在自己的理解和逻辑链下,创作出了这篇文章,如有不足,还望指出,小编会不断的修正
前言
在键盘上敲下代码到运行出结果,计算机究竟是怎么做到的,我们学习或者了解的操作系统,内存,硬盘,CPU,在这个过程中参与了哪些过程,做了什么,所有人都知道计算机的底层是01,那怎么实现的,接下来小编一一来讲解。
一、代码在编译器的过程
1.1代码在硬盘的存储形式
1.11编译器和编辑器的概念
在很多时候编辑器和编译器总是被混为一谈,这里有必要分清楚
-
编辑器 (Editor):就像一个高级记事本。它的核心工作是让你方便地编写和修改代码文本。它提供语法高亮、代码补全等功能,但它自己不会运行代码,是独立工具,需要自己配置来调用编译器。
- 例子:VS Code, Sublime Text, 记事本, Vim。
-
编译器 (Compiler):就像一个翻译官。它的核心工作是“翻译”。它读取你写的源代码(比如C++),然后把它“翻译”成CPU能直接听懂的机器语言。
- 例子:GCC, Clang, MSVC。
-
集成开发环境 (IDE):就像一个功能齐全的“一站式”工作室。它把编辑器、编译器、调试器等所有工具都打包在一起,让你在一个软件里就能完成写代码、编译、运行、调试的全部工作。
- 例子:Visual Studio, Dev-C++, CLion, PyCharm。
IDE就是为了方便用户,把编辑器、编译器这些独立的工具“打包”在一起,提供了一个统一的、好用的“外壳”。
我们这里说的编辑器其实就是指IDE,编写代码的地方
1.12代码在编辑器中的存储形式
当我们打开编辑器开始写代码,写完代码点击编译运行,我写完这些代码比如:

这些代码在编辑器中存着的,或者说就在编辑器这个进程所分配到的内存中的一块空间存着的:
编辑器作为一个正在运行的程序(也就是一个进程),它当然也需要在内存里有一块属于自己的“工作台”来处理数据。你在编辑器窗口里敲下的每一个字符(#include...),都会被编辑器这个进程暂时保存在它自己的内存空间里。这个空间里有一块区域(通常类似于堆或数据段)专门用来存放你正在编辑的文本内容。
代码在编辑器进程内存中存成 ASCII 字符,它在内存中是ASCII字符形式,但我看到的是
#include<iostream>
using namespace std;
int main()
{
cout<<1;
return 0;
}
这些啊,这是为什么
- 内存里存的是“数据”:没错,你敲下的
#字符,在内存里确实是以0x23这个数字(二进制是00100011)的形式存在的。这是给计算机自己看的。 - 屏幕上显示的是“图像”:编辑器软件有一个非常重要的功能,就是“翻译”和“绘制”。它会读取内存里的
0x23这个数字,然后去查询一个叫做“字体”的“图像字典”。 - 翻译过程:这个“字体字典”会告诉编辑器:“哦,
0x23对应的字符是#,它的图像长这样”。然后,编辑器就会调用操作系统的绘图功能,在屏幕上对应的位置,把这个#的图像画出来给你看。
1.13没编译时——代码在硬盘中得存储形式
我们点击保存时
编辑器(比如 VS Code)确实会向操作系统发出一个“保存文件”的请求,并附上你的代码内容和文件名。编辑器会调用操作系统接口,把这串存在于内存中的 ASCII 码原封不动地写入硬盘,硬盘上生成的 main.cpp 文件,其内容就是这串 ASCII 码的二进制序列
,我们主动点击一下保存,操作系统的任务是接收编辑器传来的这串ASCII码,然后负责把它写到硬盘上,:
- 首先硬盘只认识 0 和 1 , 硬盘是物理设备,它不管你是代码、图片还是小说,它只负责存储磁信号或电信号,也就是 0 和 1。所以,无论是什么文件,在硬盘底层绝对都是 01 码。
- 其次,源代码文件(.cpp或者.h文件):遵循 字符编码规则(如 ASCII 或 UTF-8)是为了让代码转化的01有规则 , 比如我敲下 int ,硬盘里存的是 01101001 (i) 01101110 (n) 01110100 (t)。int a=10; 那么在硬盘中是这样存着的:

没错,就是:
01101001(i) 01101110(n) 01110100(t) 00100000(空格)01100001(a) 01100001(空格)00111101(=) 00100000(空格)00110101(5) 00111011(;)
int a = 5; 在硬盘中就这样的形式 :
01101001 01101110 01110100 00100000 01100001 01100001 00111101 00100000 00110101 00111011
如果你用一个“十六进制编辑器”打开这个文件,你不会看到 int a = 5;,你会看到这样一串数字:
69 6E 74 20 61 20 3D 20 35 3B 没错,这就是它在硬盘上最真实的样子。
不过,数字5的ASCII码值不是0101 吗? 怎么成00110101(5)了,别急
在数学上,数字 5 的二进制应该是 101。但在你的源代码里,5 只是一个字符,就像字母 A 一样。编译器读到 00110101 时,它会想:“哦,用户写了一个字符 5”,然后它会在编译阶段把它翻译成真正的数值 101 存进机器码里。
而且回车也是有值对应的
通常我们写完一行代码会按回车键换行。如果你按了回车,硬盘里还会多出 1 到 2 个字节:
- Windows 系统:会多存
0D 0A(回车+换行)。 - Linux/Mac 系统:会多存
0A(换行)。
所以,如果算上回车,这行代码int a=10;在硬盘里的完整十六进制流大概是这样的:
69 6E 74 20 61 20 3D 20 35 3B 0
就按图示的代码,那么编译器通过操作系统在硬盘中存的就是:
#include<iostream>
using namespace std;
int main()
{
cout<<1;
return 0;
}
23 69 6E 63 6C 75 64 65 3C 69 6F 73 74 72 65 61
6D 3E 0D 0A 75 73 69 6E 67 20 6E 61 6D 65 73 70
61 63 65 20 73 74 64 3B 0D 0A 0D 0A 69 6E 74 20
6D 61 69 6E 28 29 0D 0A 7B 0D 0A 09 63 6F 75 74
3C 3C 31 3B 0D 0A 09 72 65 74 75 72 6E 20 30 3B
0D 0A 7D
这是十六进制视图,这里的十六进制数都对应着一个01二进制码哦,在底层是01,不过为了我们方便查看所以用十六进制数来表示
看着是不是像“天书”?别急,小编来带你把它还原回去。
我们把上面的十六进制数据拆开,对照 ASCII 表,看看它们是怎么变成你熟悉的代码的:
1. #include<iostream>
- 硬盘数据:
23 69 6E 63 6C 75 64 65 3C 69 6F 73 74 72 65 61 6D 3E - 解码:
23→ #69 6E 63...→ include3C→ <69 6F...→ iostream3E→ >
- 换行:
0D 0A(这是 Windows 的回车+换行,在 Linux 下只有0A)
2. using namespace std;
- 硬盘数据:
75 73 69 6E 67 20 6E 61 6D 65 73 70 61 63 65 20 73 74 64 3B - 解码:
75 73...→ using20→ 空格 (ASCII 码里的空格是0x20,也就是二进制的00100000)6E 61...→ namespace73 74 64→ std3B→ ;
3. int main()
- 硬盘数据:
69 6E 74 20 6D 61 69 6E 28 29 - 解码:
69 6E 74→ int20→ 空格6D 61 69 6E→ main28 29→ ()
4. { 和 }
- 硬盘数据:
7B...7D - 解码:
7B→ { (左大括号)7D→ } (右大括号)
5. 重点来了:cout<<1;
这里非常有意思,请注意数字 1 的存储方式。
- 硬盘数据:
63 6F 75 74 3C 3C 31 3B - 解码:
63 6F 75 74→ cout3C 3C→ << (小于号的 ASCII 是3C,两个就是3C 3C)31→ 1 (注意!这里存的是字符 '1',它的 ASCII 码是十六进制的 31,也就是二进制的00110001。它存的不是数值 1,数值 1 在二进制里应该是00000001)3B→ ;
6. return 0;
- 硬盘数据:
72 65 74 75 72 6E 20 30 3B - 解码:
72 65...→ return20→ 空格30→ 0 (字符 '0' 的 ASCII 码是30,即二进制00110000)3B→ ;
这种 01 排列是为了让人能看懂,也是为了让编译器能读懂字符(这里为什么又要让编译器读懂呢,我们后面再说)。
二、编译器是怎么存到硬盘的
我们来看一下编译器是怎么将01码存到硬盘中的
以机械硬盘为例 :

我们点击“保存”(保存源代码)时,主角是代码编辑器(VScode...)哦
- 编辑器:把你输入的字符,按照 ASCII 规则转换成对应的 01 码(比如
i变成01101001)。 - 交付:编辑器调用操作系统的接口,说:“请把这串 ASCII 码存为
main.cpp”。 - 结果:硬盘上多了一个文本文件,里面存的是 ASCII 码。
好,现在我们来看编辑器是怎么通过操作系统让硬盘来存main.cpp也就是ASCII 码的二进制序列?硬盘又是怎样工作的? 了解硬盘工作原理请点击这里硬盘原理简单入门
操作系统让硬盘来存main.cpp
编辑器(比如 VS Code)作为用户态的程序,它其实并没有权限直接去控制硬盘。它只能向操作系统“求助”。
- 动作:编辑器调用操作系统的 系统调用(System Call),通常是
write()。 - 喊话内容:“操作系统大哥,我这儿有一串数据(
main.cpp的 ASCII 码),文件名是这个,麻烦你帮我存到硬盘里去。” - 数据位置:此时,这串 ASCII 码还安安静静地躺在编辑器的内存空间里。
操作系统接到请求后,开始接管一切。它不会立刻让硬盘转动,而是先做两件事:
- 搬运数据(内存拷贝):
操作系统会把数据从“编辑器的内存”复制到“内核的缓冲区(Page Cache)”。这是为了安全,也是为了提高效率。 - 逻辑寻址(找地盘):
操作系统通过文件系统(比如 NTFS 或 ext4)在硬盘的“目录”里给main.cpp找个位置。- 它会分配一个逻辑地址(比如 LBA 1000~1005)。
- 操作系统只认这个逻辑编号,它不管数据具体在盘片的哪个角落。
操作系统把数据和逻辑地址(LBA)通过数据线(如 SATA 或 NVMe 总线)发给硬盘。这时候,硬盘上的控制器(Controller)(相当于硬盘自带的小 CPU)开始工作。
它做了最关键的一步:把逻辑地址翻译成物理指令。
- 计算位置:控制器根据 LBA,算出数据到底在哪个柱面(Cylinder)、哪个磁头(Head)、哪个扇区(Sector)。
- 比如:LBA 1000 = 第 5 圈磁道,第 2 个盘面,第 10 个扇区。
- 发出指令:控制器向硬盘的机械部件发送电流信号。
这是最硬核的部分。对于机械硬盘(HDD)来说,它通过以下动作把那串 ASCII 码变成物理痕迹:
- 寻道(移动手臂):
控制器驱动音圈电机,带着磁头臂迅速移动,把磁头精准地悬停在第 5 圈磁道上方。 - 旋转等待:
盘片高速旋转(通常是 7200转/分),直到第 10 个扇区转到磁头底下。 - 磁记录(刻写 0 和 1):
这是最后一步!- 控制器把那串 ASCII 码(比如
01101001代表 'i')转换成电流脉冲发送给磁头。 - 磁头产生微弱的磁场。
- 这个磁场改变了盘片表面涂层上微小区域的磁化方向(N极或S极)。
- N极代表
1,S极代表0(或者反过来)。(简单这样理解,具体请看硬盘笔记)
- 控制器把那串 ASCII 码(比如
在了解完代码保存的机制后,我们再来看一下当我们点击运行时,又发生了那些事情
三、编辑器点击运行后的流程
当我点击运行代码比如int a=10 ;时,编辑器会把代码以ASCII01码的方式存到硬盘中,然后编译器(比如 GCC)被编辑器在后台唤醒,它接到的任务就是处理刚才保存的那个文件,编译器再从硬盘中拿出01码进行反解出int a=10;
之后就是熟悉的预处理,编译,汇编,链接了,具体可以看gcc编译过程
编译器编译完了以后,把01机器码写回硬盘生成.exe可执行文件
值得注意的是这个文件里主要只有两类东西:
- 代码段 (.text):这里面全是 01 组成的机器指令。比如“把寄存器 A 的值加 1”、“跳转到第 10 行”。这些指令是固定的。比如:int a=10; 在.exe文件的01码中,有一串01010011... ,代表什么呢:创建一个变量a并且复制为10的动作指令码,没错 ,是动作指令码,局部变量(比如
int a = 10;写在函数里的变量)根本不会出现在这个文件里! 编译器在编译的时候,根本不会给局部变量分配具体的内存地址,更不会把“局部变量的值”写进文件里。 - 全局/静态数据段 (.data / .bss):这里面存放的是 全局变量 和 静态变量。因为它们的生命周期贯穿整个程序,所以它们必须占个固定的坑位。
当编译器在硬盘上生成.exe文件后,编译器就退场了,那接下来谁来继续工作呢?工作内容是什么呢?
接下来:
操作系统开始为你这个程序创建进程PCB,在Linux中是task_struct,在这个部分中,
关键动作是内核在 PCB 中初始化 虚拟内存管理器(对应 Linux 的 mm_struct)。什么意思呢,就是给这个程序建立虚拟地址空间,什么是虚拟地址空间:

是的,这就是一个程序虚拟地址空间,在PCB结构体中的mm_struct 中描述的是这样的空间
真实的情况是:.exe文件在硬盘中没动过,操作系统在PCB里描述了这样的空间地址:0x0000 到 0xFFFF 这块地盘放代码,其实就只由这句话,而已,其他的什么都没有变,
操作系统的加载器(Loader)会读取 .exe 文件的头部信息(PE Header 或 ELF Header)。这个文件头就像一张说明书,告诉加载器:“我的代码有多大,我的全局变量有多少,我需要多大的栈空间”。
根据这份说明书,加载器在虚拟地址空间里,为程序划定了几个关键区域的地址范围:
- 代码段:比如,从虚拟地址
0x00400000到0x00500000,用来存放程序的机器指令(01码)。 - 数据段:比如,从
0x00600000到0x00700000,用来存放已初始化的全局变量和静态变量。 - 堆(Heap):在数据段之后的一片广阔区域,用于程序运行时动态申请内存(如
malloc)。 - 栈(Stack):在虚拟地址空间的顶部(如
0x7FF00000附近),用于函数调用和局部变量。
加载器不仅划定了范围,还在mm_struct 上建立了虚拟地址与磁盘文件的对应关系。它会记录:“虚拟地址 0x00401000 的内容,对应着磁盘上 App.exe 文件从第 0x1000 个字节开始的数据”。
再次强调:这一步同样没有拷贝任何数据。它只是在账本上建立了一个索引,指明了“当需要这个虚拟地址的内容时,应该去磁盘的哪个位置找”。
当程序真正开始运行,是从CPU试图执行第一条指令开始的。这个阶段的核心是按需分配,边用边建。
-
CPU取指,触发“缺页异常”(Page Fault)
操作系统将CPU的程序计数器(PC)指向程序的入口点(例如0x00401000)也就是第一条01指令码的地址。CPU开始工作,它向内存管理单元(MMU)发出请求:“我要读取虚拟地址0x00401000的指令”。
MMU去查这个进程的页表(Page Table),结果发现,这个虚拟地址对应的物理内存页是不存在的(页表项的“存在位”为0)。于是,MMU立刻向CPU报告一个缺页异常(Page Fault)。 -
内核介入,分配物理内存
这个异常将控制权交还给操作系统内核。内核的异常处理程序会:- 检查合法性:查看这个虚拟地址
0x00401000是否在进程的“内存账本”(PCB)里。确认是合法的。 - 分配物理页框:从物理内存(RAM)的空闲区域中,找一个空闲的物理页框(假设物理地址是
0x88880000)。 - 从磁盘加载数据:内核根据之前建立的映射关系,启动DMA(直接内存访问)控制器,将
App.exe文件中偏移量为0x1000的那4KB数据,直接从硬盘复制到刚刚分配的物理内存地址0x88880000处。
- 检查合法性:查看这个虚拟地址
-
更新页表,建立映射
数据加载到物理内存后,内核会立即更新进程的页表。它在页表中写下关键的一笔:“虚拟地址0x00401000现在对应物理地址0x88880000”,并将“存在位”设为1。 -
CPU重试,成功执行
异常处理完毕后,CPU会重新执行刚才那条引发异常的指令。这一次,当它再次请求虚拟地址0x00401000时,MMU查页表发现映射已经存在,于是顺利地将虚拟地址翻译成物理地址0x88880000,并从物理内存中取出指令,交给CPU执行。
这个“访问 -> 缺页异常 -> 分配物理内存 -> 从磁盘加载 -> 更新页表 -> 重试执行”的过程,会在程序运行时不断重复。无论是访问代码段的下一条指令,还是访问数据段的全局变量,或是通过 malloc 扩展堆空间,都遵循这一机制。
那程序运行时栈和堆的虚拟内存怎么分配?
📉 栈(Stack):自动增长的“向下”地图
栈用于函数调用、局部变量等,它的特点是自动管理和向下增长(从高地址向低地址)。
1. 准备阶段:画一个“小”的初始坑位
当你双击 exe 时,操作系统的加载器在规划虚拟内存地图时,会做一件事:
- 划定范围:在虚拟地址空间的高地址处(比如
0x7FF00000附近),划定一块区域给栈。 - 初始分配:此时,操作系统并不会一下子给你划拨 1MB 或 8MB 的物理内存。它通常只在“账本”里记录:“栈的初始大小为一页(4KB)”。
- 记录界限:同时,它会在账本里记下栈的最大限制(比如 8MB,由
ulimit -s决定)。这就像在地图上画了一条红线:“栈可以往下长,但绝对不能越过这条线”。
2. 运行阶段:缺页异常的“连环触发”
程序开始运行,函数层层调用,栈空间不够了,会发生什么?
- 移动栈顶指针:CPU 有一个专门的寄存器叫栈指针(SP/RSP),它始终指向栈顶。当函数调用发生时,SP 会自动减小(因为是向下增长),指向更低的地址。
- 触发缺页异常:
- 假设当前栈用完了那初始的 4KB,SP 指向了
0x7FF00000下面的一个新地址。 - CPU 试图往这个新地址写数据(比如保存局部变量)。
- 关键点:MMU 一查页表,发现这个地址虽然在“栈的地图范围”内,但没有对应的物理内存。
- 于是,缺页异常再次发生!
- 假设当前栈用完了那初始的 4KB,SP 指向了
- 内核的智能处理:
- 内核捕获异常,检查这个地址。发现:“哦,这是栈区域,而且没有超过最大限制(8MB)。”
- 分配物理页:内核立刻从物理内存中找一个新的页框(4KB),把它映射给这个虚拟地址。
- 更新账本:在页表中记录映射关系。
- 无缝继续:CPU 重试写入,成功。
总结:栈的虚拟内存分配是“走一步,铺一步路”。你每往下调用一层函数,栈指针往下移,触碰到未分配的虚拟地址时,内核就自动给你补一页物理内存。这对程序员完全透明,你感觉栈是无限增长的(直到触碰红线,发生栈溢出)。
📈 堆(Heap):手动申请的“向上”地图
堆用于动态内存分配(malloc/new),它的特点是手动管理和向上增长(从低地址向高地址)。
1. 准备阶段:画一个“空”的预留区
在 exe 加载时,操作系统在“内存地图”的数据段(BSS段)之上,划定了一大片广阔的区域给堆。
- 状态:此时,这片区域在“账本”里是完全空白的,没有任何物理内存映射,甚至连初始的一页都没有分配。
- 边界标记:操作系统维护一个叫Program Break(堆顶指针)的标记,初始时它指向数据段的末尾。
2. 运行阶段:malloc 的“两种策略”
当你在代码里调用 p = malloc(100MB) 时,C语言运行库(如 glibc)会根据申请的大小,选择不同的系统调用来“兑现”这块虚拟内存。
策略 A:小内存(< 128KB)—— 使用 brk 系统调用
- 移动堆顶:
malloc库函数调用brk(addr)系统调用,告诉内核:“把堆顶指针(Program Break)向上移动到addr”。 - 扩展地图:内核在“账本”里更新堆的范围,确认这段虚拟地址是合法的。
- 不分配物理内存:注意!
brk调用仅仅扩展了虚拟地址空间,并没有分配物理内存。这就像你把围墙往外扩了一圈,但里面还是空地。 - 按需兑现:当你第一次使用
p指针(比如*p = 10)时,CPU 访问该地址,触发缺页异常。内核捕获后,分配物理页框,建立映射。
策略 B:大内存(> 128KB)—— 使用 mmap 系统调用
- 开辟新区:
malloc直接调用mmap系统调用。 - 寻找空地:内核在虚拟地址空间的“内存映射区”(mmap region,通常在堆和栈中间)找一块空闲的虚拟地址范围。
- 建立映射:内核在“账本”里记录这块区域用于匿名映射(不与文件关联)。
- 按需兑现:同样,此时不分配物理内存。只有当你真正读写这块内存时,才会触发缺页异常,内核才会分配物理页。
创建完PCB后:
把 PCB 的创建看作是给这个程序办好了“身份证”。有了身份证,它就是一个合法的“公民”(进程)了。但此时它还只是个空壳,没有“脑子”(内存里的代码),也没有“思想”(运行状态)。
接下来的步骤,就是操作系统给它“注入灵魂”的过程。我们将这个过程分为三个关键阶段:
🗺️第一阶段:构建虚拟世界的骨架(内存管理初始化)
PCB 建好后,内核的第一件事就是初始化内存管理结构(也就是我们以上说的 mm_struct)。
-
分配页表
内核为这个进程分配一个页全局目录(PGD)。- 此时状态:这张页表是空的。就像一本刚发下来的空白通讯录,里面还没有任何人的电话号码。
- 虚拟内存和物理内存都被划分成固定大小的块(例如4KB),分别叫“页”和“页框”。
-
解析文件头(PE/ELF)
内核读取硬盘上.exe文件的文件头。文件头里写着:“代码段有多大”、“数据段在哪里”、“入口点在哪”。 -
创建 VMA(虚拟内存区域)
内核根据文件头的信息,在mm_struct里创建一个个 VMA 节点。- VMA 1(代码段):记录“虚拟地址
0x00400000到0x00500000是代码段,权限是读+执行,数据来源是硬盘文件”。 - VMA 2(数据段):记录“虚拟地址
0x00600000到0x00700000是数据段,权限是读+写”。 - VMA 3(堆):记录“虚拟地址
0x00800000往上是堆”。 - VMA 4(栈):记录“虚拟地址
0x7FF00000附近是栈”。
关键点:这一步做完,虚拟地址空间的“地图”就画好了。但是,物理内存(RAM)里依然什么都没发生,页表里依然是空的。
- VMA 1(代码段):记录“虚拟地址
-
ps:VMA就是记录这个虚拟地址(比如 0x400000-0x500000)对应磁盘上的哪个文件(比如 exe 文件),是数据段,还是代码段,还是栈,还是堆
-
pgd就是按照映射规则,通过虚拟地址在硬盘中找到物理地址
🧵 第二阶段:注入灵魂(创建主线程)
光有地图还不行,程序得跑起来,得有人去执行代码。这就需要线程。
-
创建线程对象(TCB/ETHREAD)
操作系统在 PCB 内部(或关联位置)创建一个主线程的控制块。- 你可以把它想象成给这个进程造了一个“大脑”。
-
初始化寄存器
内核在这个线程的“上下文结构”里,设置 CPU 寄存器的初始值。其中最关键的一步是:- 设置 指令指针(EIP/RIP):把它指向程序的入口点(Entry Point)。
- 比如,设置为
0x00401000。这意味着:“程序启动后,CPU 的第一条指令要去这个地址找”。
-
加载动态链接库(DLL/SO)
通常,加载器(Loader)会介入,把程序依赖的 DLL(如kernel32.dll)也映射到虚拟地址空间中。- 这也是通过创建 VMA 完成的,同样不分配物理内存,只是建立“虚拟地址 <-> 磁盘文件”的映射关系。
🏁 第三阶段:发令枪响(切换上下文与返回用户态)
现在,PCB 有了,地图画好了,主线程准备好了,入口点也定好了。万事俱备,只欠东风。
-
调度器介入
操作系统的进程调度器把这个新进程的状态设为“就绪(Ready)”,并把它放入运行队列。 -
上下文切换
当 CPU 轮到执行这个进程时,发生了一次关键的上下文切换:- CPU 把当前正在运行的其他进程的数据保存起来。
- CPU 把页表基址寄存器(CR3) 指向新进程的页表(PGD)。这一刻起,CPU 看到的内存世界,变成了新进程的世界。
-
返回用户态(Ring 3)
CPU 的模式从 内核态(Ring 0) 切换回 用户态(Ring 3)。- CPU 根据刚才设置好的 指令指针(EIP),去取第一条指令。
⚡ 第四阶段:第一次碰撞(缺页异常)
这就是我们之前聊过的“高潮”部分:
- CPU 拿着指令指针
0x00401000去问 MMU:“我要这个地址的数据。” - MMU 查页表,发现是空的(因为之前只画了地图,没分物理内存)。
- 缺页异常爆发!
- 内核再次接管,分配物理页框,从硬盘把代码读进来,更新页表。
- CPU 重试,成功读取指令,程序正式运行!
总结
这里主要是从编写完代码后,到CPU要执行时,计算机底层发生的大致事情,
接下来待弄懂的:
- 创建PCB到CPU执行直接发生了什么
- PCB是怎样被操作系统安排的
- 操作系统做了什么
- CPU的底层运行原理
- CPU的底层结构什么
- CPU是怎样再极小的地方放下极多的电路
- CPU的工作原理
- CPU是怎样执行指令(了解了一点CPU循环执行指令的方式)
- CPU是怎样做出回应的(等等,CPU只是执行01码而已)
- CPU的寄存器是怎么配合CPU工作的
- CPU的L1,L2,L3缓存是什么
- CPU与内存的读写
- 内存的工作原理
- 内存的结构
- 操作系统的原理
(小编写作时的自我思考:
我已经知道了代码从编辑器到硬盘,再到编译器,再到硬盘形成.exe文件,
下一步是什么?:.exe文件会被操作系统拿出来放到内存里去,同时创建一个进程?
?:.exe文件放到内存上,那个内存的什么堆,栈,是什么,栈里存局部变量,申请的空间在堆里,还有什么虚拟内存,这块的知识点我很模糊,内存由哪些空间组成,.exe文件被映射到内存上,然后呢,内存又干了啥?内存怎么把一些01码给CPU呢,CPU又是怎么工作的呢?
我现在是了解内存是怎么提高CPU的速度的,是怎么缓解硬盘与CPU之间的速度差,CPU的结构,CPU的工作原理,现在把CPU的基本结构弄懂来,然后搞明白CPU的运行
目标:CPU的结构,CPU运行原理,CPU与内存之间的联系,多级缓存L1,L2,L3,
内存的工作原理,内存又是怎么运作的,操作系统又做了哪些工作)
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)