1. 并发

定义:多个进程在同一个 CPU上,靠进程切换,在一段时间内,让所有进程都往前推进。

大白话:单核 CPU 一次只能跑一个程序。操作系统快速来回切换 A、B、C 进程,看上去像同时在跑,这就是并发。

实现手段:时间片轮转。每个进程分到一小段 CPU 时间(时间片),时间用完,就切走换下一个进程。

2. 进程上下文(核心!)

CPU 里的所有寄存器(eax、ebx、eip、esp、ebp 等)存的就是当前进程的临时数据,叫进程上下文。

  • eip(程序计数器PC):记录下一条要执行指令的地址。

    系统靠 eip 知道这个进程执行到哪一行代码了。

  • eax:常用通用寄存器,C 语言return 返回值,底层就是把值放到 eax 寄存器,外部代码从 eax 拿到返回值。

例子:return a; → 汇编 mov eax, 10,把 a 的值放进 eax。

3. 进程切换的两步(最重要考点)

当进程时间片用完,要离开 CPU,操作系统做两件事:

  1. 保存上下文:把当前 CPU 所有寄存器里这个进程的数据,拷贝保存到这个进程的task_struct(进程控制块 PCB)里面。

    图里的struct reg_info就是 PCB 里面专门存放寄存器信息的结构体。

  2. 恢复上下文:轮到另一个进程上 CPU 运行时,把它 PCB 里存的寄存器数据,放回 CPU 寄存器,CPU 继续从它的 eip 地址接着跑。

一句话总结:

寄存器属于 CPU 硬件,但寄存器里面存放的数据,属于当前正在 CPU 上运行的进程。切换进程,本质就是保存 + 恢复这套寄存器数据。

4. 寄存器分类(图里写的)

  1. 通用寄存器:eax,ebx,ecx,edx,用来存计算数据
  2. 栈相关寄存器:ebp, esp, eip,控制程序执行、栈帧
  3. 状态寄存器 status:保存运算的标志(比如是否溢出、是否相等)

寄存器作用:速度极快,把高频数据放寄存器,提升 CPU 运行效率。

5. task_struct(PCB 进程控制块)

Linux 里描述一个进程的结构体task_struct,里面包含:

  • 进程 id、状态、优先级
  • struct reg_info:保存寄存器上下文
  • 进程的代码、数据、内存信息等

考点一句话背诵

进程切换 = 保存旧进程上下文 → 选新进程 → 恢复新进程上下文。

上下文就是 CPU 寄存器中属于该进程的全部临时数据。

一、环境变量

定义

环境变量是操作系统提供的一组 name=value 格式的全局变量,给进程使用,大多是全局属性。

常用 4 个:

  1. PATH:命令搜索路径。当你直接敲ls,系统会去 PATH 里面存的所有目录挨个找ls这个可执行程序。

如果不在 PATH 里,就必须写./mycmd这种完整路径运行。

  1. SHELL:当前使用的命令解释器,一般是/bin/bash
  2. USER:当前登录的用户名
  3. HOME:当前用户的家目录(cd ~ 就进到这个目录)

重点考点:子进程会继承父进程的环境变量。bash 是父进程,我们运行的程序都是 bash 创建的子进程,所以子进程拿到 bash 的环境变量。

环境变量要用 echo 命令打印查看。

正确指令

bash

运行

echo $PATH
  • echo:打印输出命令
  • $ 符号:告诉 bash,后面这个是环境变量,取变量里面保存的值

正确指令

bash

运行

echo $PATH
  • echo:打印输出命令
  • $ 符号:告诉 bash,后面这个是环境变量,取变量里面保存的值

二、main 函数的三个参数(重点!)

c

运行

int main(int argc, char *argv[], char *env[])

两张向量表(字符串指针数组,最后一个元素是NULL标记结束)

  1. argc + argv:命令行参数表
  • argc = argument count:参数个数

  • argv = argument vector:字符串指针数组,存放命令行每个参数

    示例:

bash

运行

./mycmd -a -b -c
  • argc = 4
  • argv[0] = "./mycmd" (永远是程序名!)
  • argv[1] = "-a"
  • argv[2] = "-b"
  • argv[3] = "-c"
  • argv[4] = NULL 数组末尾哨兵,标记结束

✅ 作用:给程序提供命令行选项,比如ls -l、gcc -o,就是靠 argv 拿到选项。

  1. env:环境变量表

    char *env[] 也是字符串指针数组,每一项格式 "NAME=VALUE",数组末尾同样以NULL结尾。

    在代码里可以直接遍历 env 拿到全部环境变量;

    也可以用库函数 getenv("变量名"),直接读取指定环境变量的值。

    示例:getenv("PATH"),拿到 PATH 字符串。

补充:main 的第三个参数env不是 C 语言标准,是 Linux 下扩展写法,推荐优先用getenv函数获取环境变量。


三、底层启动逻辑

我们写的main不是程序真正入口。

程序启动顺序:CRTStartup()(C 运行时启动函数)→ 准备好 argv、argc、env 三张表 → 调用 main 函数。

所以 main 才能拿到这三组数据。

四、考点:怎么验证子进程继承环境变量?

思路:写一个 C 程序,用getenv打印环境变量,在 bash 里运行它。

bash 是父进程,这个程序是 bash 创建的子进程。子进程能打印出 bash 的环境变量,就证明继承成功。

示例代码:

c

运行

#include <stdio.h>
#include <stdlib.h>
int main()
{
    printf("USER = %s\n", getenv("USER"));
    printf("HOME = %s\n", getenv("HOME"));
    printf("PATH = %s\n", getenv("PATH"));
    return 0;
}

编译运行,子进程可以输出 bash 的环境变量,验证继承。

一句话背诵

  • argc/argv:保存命令行参数;env保存环境变量,两个都是以 NULL 结尾的字符串数组。
  • 环境变量:父进程的环境变量,会自动继承给子进程。
  • PATH 是命令搜索路径,系统靠 PATH 寻找可执行命令。

1、C 语言标准规定(考试重点)

标准 C 只有 2 个合法参数版本:int main(int argc, char *argv[])

  • argc:argument count,命令行参数个数
  • argv:argument vector,字符串指针数组,存放命令行参数

还有无参版本:int main(void),没有参数。

❗ int main(int argc, char *argv[], char *env[]) 不是 C 标准!

这是Linux 系统的扩展,第三个参数env[]用来接收环境变量表,只能在 Linux/Unix 下使用,Windows、标准 C 不支持。

环境变量相关指令

📌考点汇总(背诵)

  1. 局部变量:直接var=xxx定义,env 看不到,子进程不能继承
  2. export:把局部变量导出成环境变量,子进程可继承
  3. env /printenv:只显示环境变量;set 包含局部变量
  4. unset:删除变量
  5. echo $XXX:查看单个变量

一、核心新知识点:内置命令 vs 常规命令(重点考点)

bash 里分两类命令:

  1. 常规命令(外部命令):ls、gcc、./mycmd

    执行时:bash 调用fork创建子进程,子进程用exec加载程序运行。

    子进程只能拿到父进程的环境变量,拿不到 bash 的局部变量。

  2. 内置命令(内建命令):cd、echo、export、set

    ✅ 不会创建子进程!直接在当前 bash 进程内部执行。

    这就是为什么课件例子:

    bash

    运行

    my_108=108
    echo $my_108
    

    echo是内置命令,跑在当前 bash,所以能读到局部变量my_108,输出 108。

关键区分一句话:

外部命令 → 新建子进程;内置命令 → 当前 bash 直接跑,不创建子进程。

验证方法:type 命令名

bash

运行

type echo
type ls
type cd
  • type echo:输出 echo is a shell builtin 内置命令
  • type ls:输出 ls is /usr/bin/ls 外部常规命令

type 命令作用

用来判断一条命令是【内置命令】还是【外部命令】

重点考点:cd 为什么是内置命令?

如果 cd 是外部命令:

bash 创建子进程,子进程去切换目录,子进程结束就销毁。原来 bash 当前目录一点没变,cd 就完全没用。

所以 cd 必须是内置命令,直接修改当前 bash 自己的工作目录,不开子进程。

_复习.pong

一、进程地址空间(虚拟地址空间)布局

每个进程都有一套独立的4GB 虚拟地址空间(32 位系统,地址从 0x00000000 到 0xFFFFFFFF),从低地址到高地址分成固定几段:

表格

区域位置存什么图中对应地址特点
代码区最低地址程序二进制指令、字符串常量,只读code addr: 0x401142只读不可修改
全局数据区代码区上方初始化全局变量、未初始化全局变量init global value addr: 0x40403c程序全程存在
堆区全局区上方malloc/new 动态申请的内存heap addr: 0x11a4010从低往高增长
栈区最高地址附近局部变量、函数参数、栈帧stack addr: 0x7ffe68cfb178从高往低增长

✅ 记忆口诀:栈高堆低,堆向上长,栈向下长。
一句话记忆:字符常量区属于只读区域,归在代码区,不能写。

重要考点:static 局部变量

c

运行

void func(){
    static int a = 10;
}

static 修饰的局部变量,编译时直接放到全局数据区,不是栈里。

所以函数执行完它不会销毁,地址和全局变量在同一片区域,生命周期和程序一样长。

核心主线:为什么 fork 后,父子打印变量虚拟地址一模一样,但值可以不同

一句话总纲:代码里&变量拿到的地址 = 虚拟地址,不是内存条真实的物理地址。每个进程有独立的虚拟地址空间 + 独立页表,靠页表把虚拟地址翻译成物理内存地址。

1. 图中各个模块是什么

  1. struct task_struct

    Linux 内核描述一个进程的结构体,也叫进程控制块 PCB。

    里面保存进程所有信息:pid、ppid、进程状态、页表指针、内存相关信息。父、子进程各自有自己独立的task_struct。

  2. 进程地址空间(虚拟地址空间)

    32 位机器总大小 4GB,地址范围:0x00000000 ~ 0xFFFFFFFF

    从低地址到高地址分段:

  • 代码段(正文段):存放机器指令、字符串常量,只读
  • 已初始化全局数据段 (.data):初始化全局变量,例子里g_val就在这里
  • 未初始化全局数据段 (.bss):未初始化全局变量,程序启动清零
  • 堆 heap:malloc/new 申请内存,向上增长
  • 栈 stack:局部变量、函数参数,向下增长

重点:每个进程都拥有完整独立的 4GB 虚拟地址空间,互不干扰。

  1. 页表(图中间蓝色方框)

    作用:翻译字典,虚拟地址 → 物理内存地址。

    ✅ 父进程一套页表;✅子进程一套独立页表。

  2. 物理内存(内存条硬件)

    真实硬件,所有进程共用。只有这块内存才有真正的物理地址。

2. fork 全过程(对应图里的箭头)

阶段①:父进程运行

父进程虚拟地址 0x40405c(g_val 的虚拟地址)

父进程页表:0x40405c → 映射到物理地址 0x11223344,内存存100。

阶段②:fork () 创建子进程

子进程复制父进程的虚拟地址空间、复制页表。

👉 此时父子进程页表内容完全一样,共享同一块物理内存(0x11223344,值 100)。

只复制页表,不复制物理内存!这就是写时拷贝的核心,节省时间内存。

此时父子读 g_val,拿到的都是 100,虚拟地址都是 0x40405c。

阶段③:子进程执行 g_val = 200; 【写操作!触发写时拷贝 Copy On Write】

操作系统发现:子进程要修改这块共享内存,自动做三件事:

  1. 在物理内存开辟一块全新物理内存,物理地址 0x44332211
  2. 把原来内存里的 100 复制到新内存,再改成 200
  3. 修改子进程自己的页表:子进程虚拟地址0x40405c,现在映射到新物理地址0x44332211

⚠️ 关键点:子进程的虚拟地址 0x40405c 完全不变!

父进程页表不变,依旧指向旧物理内存0x11223344,值保持 100。

3. 图上那句重点文字

重新开辟空间,但是在这个过程中,左侧的虚拟地址是 0 感知的,不影响它

大白话:C 语言代码完全看不见底层物理内存的复制、页表修改,程序视角虚拟地址永远不变。

你写的 C 代码完全感知不到操作系统在背后复制内存、修改页表。

4. 高频考点汇总(背诵版)

  1. C 语言指针、取地址&得到的地址,全部是虚拟地址,不是物理地址。
  2. fork 创建子进程,默认共享物理内存;只有写操作才触发写时拷贝。只读不拷贝。
  3. 写时拷贝:读共享,写复制。目的:加快 fork 速度,避免没必要的内存拷贝。
  4. 每个进程:独立 task_struct、独立虚拟地址空间、独立页表。
  5. 虚拟地址只是进程自己的编号,不同进程相同虚拟地址,可以映射到完全不同的物理内存。

5. 易混坑

❌ 错误理解:地址一样,内存就是同一块。

✅ 正确:虚拟地址可以相同,但是页表映射不同,物理内存是两块。

一、开篇:32 位机器为什么地址空间是 4GB

32 位地址总线,每一根线只有 0 或 1,总组合数量:(2^{32})

(2^{32}=4294967296),单位是字节 1byte

(2^{32} * 1\text{byte}=4\text{GB})

32 位计算机,地址编号从 0 ~ 2^32-1,一共 4G 个编号。

每一个编号,对应1 字节的最小内存单元。

红框那句话:

在范围内,连续的空间中,每一个最小单位都可以有地址,这个地址可以被小胖直接使用!!

大白话:

虚拟地址空间里,每 1 个字节都分配了唯一编号(地址)。写 C 代码的时候,你直接用这个编号,不用管内存条在哪。这个编号就是虚拟地址。

二、用尺子类比理解「地址空间」

图上画了一根总长 100cm 的尺子,代表整个地址空间。

小胖、小花各自占尺子上一段区间。

  • struct area:描述一段区间,start起点,end终点。

c

运行

struct area{
    int start;
    int end;
};
  • struct destop_area:管理这根尺子,记录小胖、小花各自的起止范围。

c

运行

struct destop_area{
    struct area xiaopang;
    struct area xiaohua;
};

示例:line_area = {(1,50), (51,100)}

小胖:150cm;小花:51100cm

如果要调整区间大小:

c

运行

line_area.xiaopang.end -= 10; //小胖缩短10cm
line_area.xiaohua.start -=10; //小花往前挪10cm

👉 地址空间本质:一段连续编号的地址范围,可以切分成多个子区间,记录每个区间的开始、结束,用来管理。

一句话:地址空间不是真实内存硬件,是一套编号体系。操作系统用结构体记录哪一段地址归谁。

三、Linux 内核:struct mm_struct(重点!考点)

task_struct(PCB 进程控制块)里面有一个成员 mm,类型是 struct mm_struct。

mm_struct 就是用来描述当前进程虚拟地址空间的结构体!

它记录虚拟地址空间里面各个段的起始地址、结束地址:

c

运行

struct mm_struct
{
    long code_start;    long code_end;        //代码段
    long readonly_start;long readonly_end;    //只读段(字符串常量)
    long init_start;    long init_end;        //已初始化全局数据 .data
    long uninit_start;  long uninit_end;      //未初始化全局数据 .bss
    long heap_start;    long heap_end;        //堆区
    long stack_start;   long stack_end;       //栈区
};

✅ 每个进程都有自己独立的 mm_struct,记录自己 4GB 虚拟地址空间各个段的起止位置。

✅ 堆扩容、缩容,本质就是修改heap_end这个成员变量的值。

✅ 栈的范围,记录在 stack_start、stack_end。

进程地址空间本质:内核的一个数据结构对象,和 PCB (task_struct) 一样,由操作系统维护。

操作系统先描述(用 mm_struct 记录各个段起止),再管理这片虚拟地址空间。

四、串联整套逻辑(把前面 fork 写时拷贝串起来)

  1. 32 位机器,有 0~4G 的一套地址编号(虚拟地址)。
  2. 操作系统为每个进程创建mm_struct,把 4G 虚拟地址切分成:代码段、只读段、全局数据、堆、栈。
  3. C 语言代码直接使用这套编号(&变量拿到的就是这个编号 = 虚拟地址)。
  4. 虚拟地址不是真实内存条地址,需要页表映射到物理内存。
  5. fork 创建子进程:复制父进程的mm_struct、复制页表,但是物理内存只读共享;写的时候触发写时拷贝。

五、考点背诵版

  1. 32 位地址总线:地址总数 (2^{32}),对应 4GB 地址空间。
  2. 地址空间本质:地址编号的连续范围,靠 start、end 划分不同区域。
  3. Linux 内核中,struct mm_struct 描述一个进程的虚拟地址空间,存放各个段起止地址。
  4. task_struct(PCB) 里面包含 mm_struct,PCB 是进程总的描述,mm_struct 专门管内存地址空间。
  5. 堆扩大 / 缩小,本质修改mm_struct里面 heap_end 的值。

小思考题

问:mm_struct 是存在用户空间,还是内核空间?

答:内核空间!用户写 C 代码看不到 mm_struct,只有操作系统内核才能访问。

先分 3 样东西:地址总线、数据总线、内存存储

记住:bit 是电信号的最小单位;Byte (字节) 是内存房间的单位

1. 地址总线(传递【房间号】)

  • 每一根物理线:一次只能传 1 bit(电压高 = 1,电压低 = 0)
  • 32 根地址线并行放在一起:一次送出 32 bit 的数字,这个数字就是内存房间编号(地址)
  • 32 根地址线,能组合出 (2^{32}) 种不同编号
  • 重点:(2^{32}) 个编号,对应 (2^{32}) 间房间,一间房间 = 1 Byte(8bit)

👉 地址总线的根数决定有多少个房间,不是决定房间大小。

房间大小固定:1Byte。

2. 数据总线(传递【房间里面存放的内容】)

同样:一根数据线 = 1bit

  • 8 根数据线并行:一次性读出 / 写入 8bit → 也就是 1 Byte
  • 32 根数据线并行:一次性读出 / 写入 32bit → 也就是 4 Byte

数据总线根数 = 一次能搬运多少 bit 的数据

它决定一次搬多少东西,不决定一共有多少房间。

3. 内存(存放数据)

内存最小可单独访问的单元:1 Byte(8 bit)

一间房间 = 1Byte,房间里面存 8 个 bit(8 个 0/1)

你不能单独只拿出房间里其中 1 个 bit,必须一次性拿整个房间(1Byte)

大饼比喻(核心!虚拟地址空间)

老美(操作系统)有 10 亿美金(真实物理内存)。

但是给 4 个孩子(4 个进程)每人画一张大饼,每个人都以为自己拥有完整 10 亿家产。

  • 每个进程都觉得自己独占完整 4GB 地址空间(大饼)
  • 进程之间互相看不见对方,不知道别人也有一张一模一样的大饼
  • 大饼是虚拟的,不是真的每人都有 4GB 物理内存条

这就是进程虚拟地址空间。

三、为什么操作系统要设计虚拟地址空间(3 个核心作用,考试考点)

1. 让进程以统一的视角看待内存

没有虚拟地址的话:物理内存地址是零散的。程序要关心内存哪里空闲、哪里被别的程序占用,写代码极其麻烦。

有虚拟地址后:

每个进程看到的都是从 0 开始连续完整的 4GB 虚拟地址。

程序员写 C 代码直接用虚拟地址,不用管真实物理内存怎么放。

程序视角:内存是一整块连续空间。

真实硬件:物理内存可以碎片化,零散分布。

2. 内存保护(安全)

虚拟地址访问内存,必须经过页表转换。

操作系统在地址翻译这一步,可以检查这次访问合不合法:

  • 如果进程试图访问不属于自己的虚拟地址(比如想去读别的进程的内存)

  • 操作系统直接拦截,不会把请求送到物理内存,直接触发段错误 segment fault

    👉 作用:A 进程不能随便篡改 B 进程的数据,隔离保护物理内存。

3. 解耦:进程管理模块 和 内存管理模块分开

解耦大白话:两件事分开管理,互不绑定。

  • 进程管理:只管进程生命周期(创建、调度、销毁)

  • 内存管理:只管物理内存分配、页表映射

    好处:

    进程不用绑定固定的物理内存。进程可以换内存、换页,甚至交换到磁盘(swap),进程本身代码完全不用修改。

    物理内存不够的时候,可以把暂时不用的内存页面挪到硬盘,进程感知不到。

四、整张图逻辑串起来

  1. OS 给每一个进程,独立分配一套虚拟地址空间(大饼)。
  2. 每个进程都认为自己独占全部内存,互相隔离,看不到彼此。
  3. 进程访问内存时,虚拟地址通过页表翻译成物理地址。
  4. 翻译的过程中做权限检查,非法访问直接拦截,实现内存保护。
  5. 内核靠 task_struct、mm_struct、页表这一套结构体管理进程和它的虚拟内存。

考点背诵精简版

  1. 进程 = 内核数据结构(task_struct、mm_struct、页表)+ 代码与数据。

  2. 虚拟地址空间:给每个进程虚构一套完整连续地址,每个进程独立,互相隔离。

  3. 三大意义:

    ① 统一内存视角,简化程序编写;

    ② 地址转换时做访问检查,实现内存保护;

    ③ 解耦进程管理和内存管理。

1. 左边内核结构体

  • task_struct:进程 PCB,里面有指针 mm_struct *mm

  • mm_struct:描述当前进程 4GB 虚拟地址空间,划分:

    用户空间 3G + 内核空间 1G

    从低地址到高地址:正文代码段 → 初始化数据 → 未初始化 bss → 堆 → 共享区 → 栈

每个进程都有自己独立的mm_struct、独立页表。

2. CR3 寄存器(重点!硬件上下文)

CPU 里面的 cr3 寄存器,存放当前进程页表的物理地址。

  • 切换进程的时候,操作系统会修改 cr3 的值,换成新进程的页表地址。

  • 一旦 cr3 换了,页表就换了,同一个虚拟地址,翻译出来的物理内存就变了。

    👉 这就是进程内存独立性的硬件基础。

图里写:本质属于进程的硬件上下文。

进程上下文 = 进程运行时所有需要的状态信息总和。

分为两大块:软件上下文 + 硬件上下文

① 软件上下文(放在内存里,内核结构体)

就是内核维护的数据,进程不在 CPU 上跑的时候也保存在内存。

  • task_struct(PCB)
  • mm_struct、页表相关信息
  • 文件描述符、信号、内存段信息等

② 硬件上下文(CPU 寄存器里面的值)

CPU 正在运行这个进程时,寄存器里临时存放的状态。一旦切走进程,必须保存;再次运行时恢复。

包含:

  • 通用寄存器 eax ebx ecx edx
  • 栈寄存器 esp、ebp
  • 指令寄存器 eip(下一条要执行代码的地址)
  • ✅ CR3 寄存器(页表物理地址,属于硬件上下文!)

重点区分:

  • 软件上下文:在内存,长期存放,进程休眠时也在。
  • 硬件上下文:在 CPU 寄存器,只有进程在 CPU 上跑的时候才占用寄存器。一旦切换进程,寄存器就被覆盖,所以要先保存。

3. 页表是干什么的

页表就是一张翻译表:虚拟地址 → 物理地址,同时记录权限(r 只读 /rw 读写)

示例表格:

表格

虚拟地址物理地址权限
0x1234560x321rw
0x11110x12r

举例子:CPU 访问虚拟地址0x1111

  1. CPU 拿 cr3 找到当前进程页表
  2. 查询页表:虚拟 0x1111 → 物理地址 0x12,权限是 r(只读)
  3. 如果代码尝试对这个地址写数据,权限校验失败,直接报错段错误。

代码段、字符常量区标记为只读,就是在页表里设置权限位,防止程序误修改代码。

代码段(正文段 .text)

存放:编译好的机器指令(二进制 CPU 指令,就是程序要执行的代码)

比如你写的 C 语言printf、if、循环,编译后变成一条条机器指令,全部放在这里。

✅ 权限:只读

字符常量区(.rodata,只读数据段)

存放:字符串常量、const 修饰的全局常量

例:char *p = "hello";,"hello"这个字符串就放在这里。

✅ 权限:只读


为什么这两块区域设置成只读(考点)

  1. 保护指令,防止被篡改

    程序运行的机器指令不能随便修改。如果代码段可写,程序跑着跑着不小心改写指令,直接崩溃,甚至被恶意代码攻击。

  2. 支持内存共享,节省物理内存

    多个进程同时运行同一个程序(比如开多个 bash 窗口),它们的代码是一模一样的。

    操作系统可以让多个进程的虚拟地址,映射到同一份物理内存里的代码。

    因为标记为只读,大家只能读,谁都改不了,共享就安全。

举例子:打开 3 个 xshell,运行 3 份 bash 进程。

3 个 bash 进程,共享同一份物理内存里的 bash 代码,不用在内存存 3 份代码,节约内存。

  1. 常量本身就不允许修改

    "hello"字符串常量,语法上就不允许修改。放到只读区域,一旦尝试写,硬件 + 页表直接拦截,触发段错误。

3. 内核空间放在最高地址

内核是操作系统本身,所有进程共享同一份内核虚拟地址。

放到整个 4G 地址的最顶端,直接和用户空间划一条分界线。

用户进程不能随便访问内核空间,硬件 + 页表做权限拦截。

4. 惰性加载(懒加载 + 缺页中断,核心考点)

共识:现代操作系统,几乎不做任何浪费空间和浪费时间的事情

问题:创建进程的时候,先建内核结构体,还是先把磁盘可执行程序全部加载进内存?

✅ 先创建内核数据结构(task_struct、mm_struct、页表),不会一次性把整个程序全部读到内存!

一次性把大程序全部载入内存很慢、浪费内存。操作系统用惰性加载。

流程:

  1. 刚创建进程:只建立虚拟地址空间、页表。代码数据还留在磁盘,没有放进物理内存。

  2. 进程运行,访问某一块虚拟地址。

  3. CPU 查页表,发现:这块虚拟地址对应的物理内存不存在 → 触发缺页中断。

  4. 进入内核的缺页中断处理函数:

    • 操作系统去磁盘,把需要的那一页代码,加载到物理内存
    • 修改页表,填上物理地址,标记有效
    • 返回用户态,重新执行刚才出错的指令。

一句话:用到哪一页,才加载哪一页,不用的就留在磁盘。

好处:大文件分批加载,节省内存,进程启动速度更快。

图上问题:你怎么知道进程的代码数据在不在内存?

答:看页表里面的有效位(存在位),有效位 = 0 → 不在内存,触发缺页中断。

5. 物理内存 ↔ 磁盘

物理内存容量有限,操作系统还可以把暂时不用的页面,换出到磁盘 swap 分区;等需要的时候再换回内存。进程完全感知不到。

6. 全部考点汇总(背诵版)

  1. cr3 寄存器:存放当前进程页表物理地址;进程切换时修改 cr3,实现地址空间切换。
  2. 页表两个作用:①虚拟地址转物理地址;②权限检查(r/rw),实现内存保护。
  3. 代码段、常量区在页表设置只读权限,保护指令,支持多进程共享代码。
  4. 创建进程:先创建内核结构体,不一次性加载全部程序到内存。
  5. 惰性加载:按需加载,访问页面时,缺页中断把磁盘数据载入内存。
  6. 缺页中断:访问虚拟地址,页表发现页面不在物理内存,触发中断,由 OS 加载页面。
Logo

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

更多推荐