作为一名计科学生,每天学来学去,有一天突然发现自己还不清楚每天写的代码的运行流程,内心感到羞愧不已,于是下定决心一定要弄懂不会的来,所以不断的找寻相关知识,在自己的理解和逻辑链下,创作出了这篇文章,如有不足,还望指出,小编会不断的修正


前言

在键盘上敲下代码到运行出结果,计算机究竟是怎么做到的,我们学习或者了解的操作系统,内存,硬盘,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;
} 

 这些啊,这是为什么

  1. 内存里存的是“数据”:没错,你敲下的 # 字符,在内存里确实是以 0x23 这个数字(二进制是 00100011)的形式存在的。这是给计算机自己看的。
  2. 屏幕上显示的是“图像”:编辑器软件有一个非常重要的功能,就是“翻译”和“绘制”。它会读取内存里的 0x23 这个数字,然后去查询一个叫做“字体”的“图像字典”。
  3. 翻译过程:这个“字体字典”会告诉编辑器:“哦,0x23 对应的字符是 #,它的图像长这样”。然后,编辑器就会调用操作系统的绘图功能,在屏幕上对应的位置,把这个 # 的图像画出来给你看。

1.13没编译时——代码在硬盘中得存储形式

我们点击保存时

编辑器(比如 VS Code)确实会向操作系统发出一个“保存文件”的请求,并附上你的代码内容和文件名。编辑器会调用操作系统接口,把这串存在于内存中的 ASCII 码原封不动地写入硬盘,硬盘上生成的 main.cpp 文件,其内容就是这串 ASCII 码的二进制序列

,我们主动点击一下保存,操作系统的任务是接收编辑器传来的这串ASCII码,然后负责把它写到硬盘上,:

  1. 首先硬盘只认识 0 和 1 , 硬盘是物理设备,它不管你是代码、图片还是小说,它只负责存储磁信号或电信号,也就是 0 和 1。所以,无论是什么文件,在硬盘底层绝对都是 01 码。
  2. 其次,源代码文件(.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... → include
    • 3C → <
    • 69 6F... → iostream
    • 3E → >
  • 换行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... → using
    • 20 → 空格 (ASCII 码里的空格是 0x20,也就是二进制的 00100000)
    • 6E 61... → namespace
    • 73 74 64 → std
    • 3B → ;

3. int main()

  • 硬盘数据69 6E 74 20 6D 61 69 6E 28 29
  • 解码:
    • 69 6E 74 → int
    • 20 → 空格
    • 6D 61 69 6E → main
    • 28 29 → ()

4. { 和 }

  • 硬盘数据7B ... 7D
  • 解码:
    • 7B → { (左大括号)
    • 7D → } (右大括号)

5. 重点来了:cout<<1;

这里非常有意思,请注意数字 1 的存储方式。

  • 硬盘数据63 6F 75 74 3C 3C 31 3B
  • 解码:
    • 63 6F 75 74 → cout
    • 3C 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... → return
    • 20 → 空格
    • 30 → 0 (字符 '0' 的 ASCII 码是 30,即二进制 00110000)
    • 3B → ;

这种 01 排列是为了让人能看懂,也是为了让编译器能读懂字符(这里为什么又要让编译器读懂呢,我们后面再说)。


二、编译器是怎么存到硬盘的

我们来看一下编译器是怎么将01码存到硬盘中的

以机械硬盘为例 :

我们点击“保存”(保存源代码)时,主角是代码编辑器(VScode...)哦

  1. 编辑器:把你输入的字符,按照 ASCII 规则转换成对应的 01 码(比如 i 变成 01101001)。
  2. 交付:编辑器调用操作系统的接口,说:“请把这串 ASCII 码存为 main.cpp”。
  3. 结果:硬盘上多了一个文本文件,里面存的是 ASCII 码。

好,现在我们来看编辑器是怎么通过操作系统让硬盘来存main.cpp也就是ASCII 码的二进制序列?硬盘又是怎样工作的? 了解硬盘工作原理请点击这里硬盘原理简单入门

操作系统让硬盘来存main.cpp

编辑器(比如 VS Code)作为用户态的程序,它其实并没有权限直接去控制硬盘。它只能向操作系统“求助”。

  • 动作:编辑器调用操作系统的 系统调用(System Call),通常是 write()
  • 喊话内容:“操作系统大哥,我这儿有一串数据(main.cpp 的 ASCII 码),文件名是这个,麻烦你帮我存到硬盘里去。”
  • 数据位置:此时,这串 ASCII 码还安安静静地躺在编辑器的内存空间里。

操作系统接到请求后,开始接管一切。它不会立刻让硬盘转动,而是先做两件事:

  1. 搬运数据(内存拷贝)
    操作系统会把数据从“编辑器的内存”复制到“内核的缓冲区(Page Cache)”。这是为了安全,也是为了提高效率。
  2. 逻辑寻址(找地盘)
    操作系统通过文件系统(比如 NTFS 或 ext4)在硬盘的“目录”里给 main.cpp 找个位置。
    • 它会分配一个逻辑地址(比如 LBA 1000~1005)。
    • 操作系统只认这个逻辑编号,它不管数据具体在盘片的哪个角落。

操作系统把数据和逻辑地址(LBA)通过数据线(如 SATA 或 NVMe 总线)发给硬盘。这时候,硬盘上的控制器(Controller)(相当于硬盘自带的小 CPU)开始工作。

它做了最关键的一步:把逻辑地址翻译成物理指令

  • 计算位置:控制器根据 LBA,算出数据到底在哪个柱面(Cylinder)、哪个磁头(Head)、哪个扇区(Sector)
    • 比如:LBA 1000 = 第 5 圈磁道,第 2 个盘面,第 10 个扇区。
  • 发出指令:控制器向硬盘的机械部件发送电流信号。

这是最硬核的部分。对于机械硬盘(HDD)来说,它通过以下动作把那串 ASCII 码变成物理痕迹:

  1. 寻道(移动手臂)
    控制器驱动音圈电机,带着磁头臂迅速移动,把磁头精准地悬停在第 5 圈磁道上方。
  2. 旋转等待
    盘片高速旋转(通常是 7200转/分),直到第 10 个扇区转到磁头底下。
  3. 磁记录(刻写 0 和 1)
    这是最后一步!
    • 控制器把那串 ASCII 码(比如 01101001 代表 'i')转换成电流脉冲发送给磁头。
    • 磁头产生微弱的磁场
    • 这个磁场改变了盘片表面涂层上微小区域的磁化方向(N极或S极)。
    • N极代表 1S极代表 0(或者反过来)。(简单这样理解,具体请看硬盘笔记

在了解完代码保存的机制后,我们再来看一下当我们点击运行时,又发生了那些事情


三、编辑器点击运行后的流程

当我点击运行代码比如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试图执行第一条指令开始的。这个阶段的核心是按需分配,边用边建

  1. CPU取指,触发“缺页异常”(Page Fault)
    操作系统将CPU的程序计数器(PC)指向程序的入口点(例如 0x00401000)也就是第一条01指令码的地址。CPU开始工作,它向内存管理单元(MMU)发出请求:“我要读取虚拟地址 0x00401000 的指令”。
    MMU去查这个进程的页表(Page Table),结果发现,这个虚拟地址对应的物理内存页是不存在的(页表项的“存在位”为0)。于是,MMU立刻向CPU报告一个缺页异常(Page Fault)

  2. 内核介入,分配物理内存
    这个异常将控制权交还给操作系统内核。内核的异常处理程序会:

    • 检查合法性:查看这个虚拟地址 0x00401000 是否在进程的“内存账本”(PCB)里。确认是合法的。
    • 分配物理页框:从物理内存(RAM)的空闲区域中,找一个空闲的物理页框(假设物理地址是 0x88880000)。
    • 从磁盘加载数据:内核根据之前建立的映射关系,启动DMA(直接内存访问)控制器,将 App.exe 文件中偏移量为 0x1000 的那4KB数据,直接从硬盘复制到刚刚分配的物理内存地址 0x88880000 处。
  3. 更新页表,建立映射
    数据加载到物理内存后,内核会立即更新进程的页表。它在页表中写下关键的一笔:“虚拟地址 0x00401000 现在对应物理地址 0x88880000,并将“存在位”设为1。

  4. CPU重试,成功执行
    异常处理完毕后,CPU会重新执行刚才那条引发异常的指令。这一次,当它再次请求虚拟地址 0x00401000 时,MMU查页表发现映射已经存在,于是顺利地将虚拟地址翻译成物理地址 0x88880000,并从物理内存中取出指令,交给CPU执行。

这个“访问 -> 缺页异常 -> 分配物理内存 -> 从磁盘加载 -> 更新页表 -> 重试执行”的过程,会在程序运行时不断重复。无论是访问代码段的下一条指令,还是访问数据段的全局变量,或是通过 malloc 扩展堆空间,都遵循这一机制。

那程序运行时栈和堆的虚拟内存怎么分配?

📉 栈(Stack):自动增长的“向下”地图

栈用于函数调用、局部变量等,它的特点是自动管理向下增长(从高地址向低地址)。

1. 准备阶段:画一个“小”的初始坑位

当你双击 exe 时,操作系统的加载器在规划虚拟内存地图时,会做一件事:

  • 划定范围:在虚拟地址空间的高地址处(比如 0x7FF00000 附近),划定一块区域给栈。
  • 初始分配:此时,操作系统并不会一下子给你划拨 1MB 或 8MB 的物理内存。它通常只在“账本”里记录:“栈的初始大小为一页(4KB)”
  • 记录界限:同时,它会在账本里记下栈的最大限制(比如 8MB,由 ulimit -s 决定)。这就像在地图上画了一条红线:“栈可以往下长,但绝对不能越过这条线”。

2. 运行阶段:缺页异常的“连环触发”

程序开始运行,函数层层调用,栈空间不够了,会发生什么?

  1. 移动栈顶指针:CPU 有一个专门的寄存器叫栈指针(SP/RSP),它始终指向栈顶。当函数调用发生时,SP 会自动减小(因为是向下增长),指向更低的地址。
  2. 触发缺页异常
    • 假设当前栈用完了那初始的 4KB,SP 指向了 0x7FF00000 下面的一个新地址。
    • CPU 试图往这个新地址写数据(比如保存局部变量)。
    • 关键点:MMU 一查页表,发现这个地址虽然在“栈的地图范围”内,但没有对应的物理内存
    • 于是,缺页异常再次发生!
  3. 内核的智能处理
    • 内核捕获异常,检查这个地址。发现:“哦,这是栈区域,而且没有超过最大限制(8MB)。”
    • 分配物理页:内核立刻从物理内存中找一个新的页框(4KB),把它映射给这个虚拟地址。
    • 更新账本:在页表中记录映射关系。
  4. 无缝继续:CPU 重试写入,成功。

总结:栈的虚拟内存分配是“走一步,铺一步路”。你每往下调用一层函数,栈指针往下移,触碰到未分配的虚拟地址时,内核就自动给你补一页物理内存。这对程序员完全透明,你感觉栈是无限增长的(直到触碰红线,发生栈溢出)。


📈 堆(Heap):手动申请的“向上”地图

堆用于动态内存分配(malloc/new),它的特点是手动管理向上增长(从低地址向高地址)。

1. 准备阶段:画一个“空”的预留区

在 exe 加载时,操作系统在“内存地图”的数据段(BSS段)之上,划定了一大片广阔的区域给堆。

  • 状态:此时,这片区域在“账本”里是完全空白的,没有任何物理内存映射,甚至连初始的一页都没有分配。
  • 边界标记:操作系统维护一个叫Program Break(堆顶指针)的标记,初始时它指向数据段的末尾。

2. 运行阶段:malloc 的“两种策略”

当你在代码里调用 p = malloc(100MB) 时,C语言运行库(如 glibc)会根据申请的大小,选择不同的系统调用来“兑现”这块虚拟内存。

策略 A:小内存(< 128KB)—— 使用 brk 系统调用

  1. 移动堆顶malloc 库函数调用 brk(addr) 系统调用,告诉内核:“把堆顶指针(Program Break)向上移动到 addr”。
  2. 扩展地图:内核在“账本”里更新堆的范围,确认这段虚拟地址是合法的。
  3. 不分配物理内存:注意!brk 调用仅仅扩展了虚拟地址空间,并没有分配物理内存。这就像你把围墙往外扩了一圈,但里面还是空地。
  4. 按需兑现:当你第一次使用 p 指针(比如 *p = 10)时,CPU 访问该地址,触发缺页异常。内核捕获后,分配物理页框,建立映射。

策略 B:大内存(> 128KB)—— 使用 mmap 系统调用

  1. 开辟新区malloc 直接调用 mmap 系统调用。
  2. 寻找空地:内核在虚拟地址空间的“内存映射区”(mmap region,通常在堆和栈中间)找一块空闲的虚拟地址范围。
  3. 建立映射:内核在“账本”里记录这块区域用于匿名映射(不与文件关联)。
  4. 按需兑现:同样,此时不分配物理内存。只有当你真正读写这块内存时,才会触发缺页异常,内核才会分配物理页。

创建完PCB后:

把 PCB 的创建看作是给这个程序办好了“身份证”。有了身份证,它就是一个合法的“公民”(进程)了。但此时它还只是个空壳,没有“脑子”(内存里的代码),也没有“思想”(运行状态)。

接下来的步骤,就是操作系统给它“注入灵魂”的过程。我们将这个过程分为三个关键阶段:

🗺️第一阶段:构建虚拟世界的骨架(内存管理初始化)

PCB 建好后,内核的第一件事就是初始化内存管理结构(也就是我们以上说的 mm_struct)。

  1. 分配页表
    内核为这个进程分配一个页全局目录(PGD)

    • 此时状态:这张页表是空的。就像一本刚发下来的空白通讯录,里面还没有任何人的电话号码。
    • 虚拟内存物理内存都被划分成固定大小的块(例如4KB),分别叫“”和“页框”。
  2. 解析文件头(PE/ELF)
    内核读取硬盘上 .exe 文件的文件头。文件头里写着:“代码段有多大”、“数据段在哪里”、“入口点在哪”。

  3. 创建 VMA(虚拟内存区域)
    内核根据文件头的信息,在 mm_struct 里创建一个个 VMA 节点。

    • VMA 1(代码段):记录“虚拟地址 0x00400000 到 0x00500000 是代码段,权限是读+执行,数据来源是硬盘文件”。
    • VMA 2(数据段):记录“虚拟地址 0x00600000 到 0x00700000 是数据段,权限是读+写”。
    • VMA 3(堆):记录“虚拟地址 0x00800000 往上是堆”。
    • VMA 4(栈):记录“虚拟地址 0x7FF00000 附近是栈”。

    关键点:这一步做完,虚拟地址空间的“地图”就画好了。但是,物理内存(RAM)里依然什么都没发生,页表里依然是空的。

  4. ps:VMA就是记录这个虚拟地址(比如 0x400000-0x500000)对应磁盘上的哪个文件(比如 exe 文件),是数据段,还是代码段,还是栈,还是堆

  5. pgd就是按照映射规则,通过虚拟地址在硬盘中找到物理地址

🧵 第二阶段:注入灵魂(创建主线程)

光有地图还不行,程序得跑起来,得有人去执行代码。这就需要线程

  1. 创建线程对象(TCB/ETHREAD)
    操作系统在 PCB 内部(或关联位置)创建一个主线程的控制块。

    • 你可以把它想象成给这个进程造了一个“大脑”。
  2. 初始化寄存器
    内核在这个线程的“上下文结构”里,设置 CPU 寄存器的初始值。其中最关键的一步是:

    • 设置 指令指针(EIP/RIP):把它指向程序的入口点(Entry Point)。
    • 比如,设置为 0x00401000。这意味着:“程序启动后,CPU 的第一条指令要去这个地址找”。
  3. 加载动态链接库(DLL/SO)
    通常,加载器(Loader)会介入,把程序依赖的 DLL(如 kernel32.dll)也映射到虚拟地址空间中。

    • 这也是通过创建 VMA 完成的,同样不分配物理内存,只是建立“虚拟地址 <-> 磁盘文件”的映射关系。

🏁 第三阶段:发令枪响(切换上下文与返回用户态)

现在,PCB 有了,地图画好了,主线程准备好了,入口点也定好了。万事俱备,只欠东风。

  1. 调度器介入
    操作系统的进程调度器把这个新进程的状态设为“就绪(Ready)”,并把它放入运行队列。

  2. 上下文切换
    当 CPU 轮到执行这个进程时,发生了一次关键的上下文切换

    • CPU 把当前正在运行的其他进程的数据保存起来。
    • CPU 把页表基址寄存器(CR3) 指向新进程的页表(PGD)。这一刻起,CPU 看到的内存世界,变成了新进程的世界。
  3. 返回用户态(Ring 3)
    CPU 的模式从 内核态(Ring 0) 切换回 用户态(Ring 3)

    • CPU 根据刚才设置好的 指令指针(EIP),去取第一条指令。

⚡ 第四阶段:第一次碰撞(缺页异常)

这就是我们之前聊过的“高潮”部分:

  1. CPU 拿着指令指针 0x00401000 去问 MMU:“我要这个地址的数据。”
  2. MMU 查页表,发现是空的(因为之前只画了地图,没分物理内存)。
  3. 缺页异常爆发!
  4. 内核再次接管,分配物理页框,从硬盘把代码读进来,更新页表。
  5. CPU 重试,成功读取指令,程序正式运行!


总结

这里主要是从编写完代码后,到CPU要执行时,计算机底层发生的大致事情,

接下来待弄懂的:

  1. 创建PCB到CPU执行直接发生了什么
    1. PCB是怎样被操作系统安排的
    2. 操作系统做了什么
  2. CPU的底层运行原理
    1. CPU的底层结构什么
    2. CPU是怎样再极小的地方放下极多的电路
  3. CPU的工作原理
    1. CPU是怎样执行指令(了解了一点CPU循环执行指令的方式)
    2. CPU是怎样做出回应的(等等,CPU只是执行01码而已)
    3. CPU的寄存器是怎么配合CPU工作的
    4. CPU的L1,L2,L3缓存是什么
  4. CPU与内存的读写
  5. 内存的工作原理
    1. 内存的结构
  6. 操作系统的原理

(小编写作时的自我思考:
我已经知道了代码从编辑器到硬盘,再到编译器,再到硬盘形成.exe文件,

下一步是什么?:.exe文件会被操作系统拿出来放到内存里去,同时创建一个进程?

?:.exe文件放到内存上,那个内存的什么堆,栈,是什么,栈里存局部变量,申请的空间在堆里,还有什么虚拟内存,这块的知识点我很模糊,内存由哪些空间组成,.exe文件被映射到内存上,然后呢,内存又干了啥?内存怎么把一些01码给CPU呢,CPU又是怎么工作的呢?

我现在是了解内存是怎么提高CPU的速度的,是怎么缓解硬盘与CPU之间的速度差,CPU的结构,CPU的工作原理,现在把CPU的基本结构弄懂来,然后搞明白CPU的运行

目标:CPU的结构,CPU运行原理,CPU与内存之间的联系,多级缓存L1,L2,L3,

内存的工作原理,内存又是怎么运作的,操作系统又做了哪些工作)

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐