Linux env环境变量与进程地址空间
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,操作系统做两件事:
-
保存上下文:把当前 CPU 所有寄存器里这个进程的数据,拷贝保存到这个进程的
task_struct(进程控制块 PCB)里面。图里的
struct reg_info就是 PCB 里面专门存放寄存器信息的结构体。 -
恢复上下文:轮到另一个进程上 CPU 运行时,把它 PCB 里存的寄存器数据,放回 CPU 寄存器,CPU 继续从它的 eip 地址接着跑。
一句话总结:
寄存器属于 CPU 硬件,但寄存器里面存放的数据,属于当前正在 CPU 上运行的进程。切换进程,本质就是保存 + 恢复这套寄存器数据。
4. 寄存器分类(图里写的)
- 通用寄存器:
eax,ebx,ecx,edx,用来存计算数据 - 栈相关寄存器:
ebp, esp, eip,控制程序执行、栈帧 - 状态寄存器 status:保存运算的标志(比如是否溢出、是否相等)
寄存器作用:速度极快,把高频数据放寄存器,提升 CPU 运行效率。
5. task_struct(PCB 进程控制块)
Linux 里描述一个进程的结构体task_struct,里面包含:
- 进程 id、状态、优先级
struct reg_info:保存寄存器上下文- 进程的代码、数据、内存信息等
考点一句话背诵
进程切换 = 保存旧进程上下文 → 选新进程 → 恢复新进程上下文。
上下文就是 CPU 寄存器中属于该进程的全部临时数据。
一、环境变量
定义
环境变量是操作系统提供的一组 name=value 格式的全局变量,给进程使用,大多是全局属性。
常用 4 个:
- PATH:命令搜索路径。当你直接敲
ls,系统会去 PATH 里面存的所有目录挨个找ls这个可执行程序。
如果不在 PATH 里,就必须写
./mycmd这种完整路径运行。
- SHELL:当前使用的命令解释器,一般是
/bin/bash - USER:当前登录的用户名
- 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标记结束)
- argc + argv:命令行参数表
-
argc= argument count:参数个数 -
argv= argument vector:字符串指针数组,存放命令行每个参数示例:
bash
运行
./mycmd -a -b -c
argc = 4argv[0] = "./mycmd"(永远是程序名!)argv[1] = "-a"argv[2] = "-b"argv[3] = "-c"argv[4] = NULL数组末尾哨兵,标记结束
✅ 作用:给程序提供命令行选项,比如ls -l、gcc -o,就是靠 argv 拿到选项。
-
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 不支持。
环境变量相关指令
📌考点汇总(背诵)
- 局部变量:直接
var=xxx定义,env 看不到,子进程不能继承 - export:把局部变量导出成环境变量,子进程可继承
- env /printenv:只显示环境变量;set 包含局部变量
- unset:删除变量
echo $XXX:查看单个变量
一、核心新知识点:内置命令 vs 常规命令(重点考点)
bash 里分两类命令:
-
常规命令(外部命令):
ls、gcc、./mycmd执行时:bash 调用
fork创建子进程,子进程用exec加载程序运行。子进程只能拿到父进程的环境变量,拿不到 bash 的局部变量。
-
内置命令(内建命令):
cd、echo、export、set✅ 不会创建子进程!直接在当前 bash 进程内部执行。
这就是为什么课件例子:
bash
运行
my_108=108 echo $my_108echo是内置命令,跑在当前 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. 图中各个模块是什么
-
struct task_struct
Linux 内核描述一个进程的结构体,也叫进程控制块 PCB。
里面保存进程所有信息:pid、ppid、进程状态、页表指针、内存相关信息。父、子进程各自有自己独立的
task_struct。 -
进程地址空间(虚拟地址空间)
32 位机器总大小 4GB,地址范围:
0x00000000 ~ 0xFFFFFFFF从低地址到高地址分段:
- 代码段(正文段):存放机器指令、字符串常量,只读
- 已初始化全局数据段 (.data):初始化全局变量,例子里
g_val就在这里 - 未初始化全局数据段 (.bss):未初始化全局变量,程序启动清零
- 堆 heap:malloc/new 申请内存,向上增长
- 栈 stack:局部变量、函数参数,向下增长
重点:每个进程都拥有完整独立的 4GB 虚拟地址空间,互不干扰。
-
页表(图中间蓝色方框)
作用:翻译字典,虚拟地址 → 物理内存地址。
✅ 父进程一套页表;✅子进程一套独立页表。
-
物理内存(内存条硬件)
真实硬件,所有进程共用。只有这块内存才有真正的物理地址。
2. fork 全过程(对应图里的箭头)
阶段①:父进程运行
父进程虚拟地址 0x40405c(g_val 的虚拟地址)
父进程页表:0x40405c → 映射到物理地址 0x11223344,内存存100。
阶段②:fork () 创建子进程
子进程复制父进程的虚拟地址空间、复制页表。
👉 此时父子进程页表内容完全一样,共享同一块物理内存(0x11223344,值 100)。
只复制页表,不复制物理内存!这就是写时拷贝的核心,节省时间内存。
此时父子读 g_val,拿到的都是 100,虚拟地址都是 0x40405c。
阶段③:子进程执行 g_val = 200; 【写操作!触发写时拷贝 Copy On Write】
操作系统发现:子进程要修改这块共享内存,自动做三件事:
- 在物理内存开辟一块全新物理内存,物理地址
0x44332211 - 把原来内存里的 100 复制到新内存,再改成 200
- 修改子进程自己的页表:子进程虚拟地址
0x40405c,现在映射到新物理地址0x44332211
⚠️ 关键点:子进程的虚拟地址 0x40405c 完全不变!
父进程页表不变,依旧指向旧物理内存
0x11223344,值保持 100。
3. 图上那句重点文字
重新开辟空间,但是在这个过程中,左侧的虚拟地址是 0 感知的,不影响它
大白话:C 语言代码完全看不见底层物理内存的复制、页表修改,程序视角虚拟地址永远不变。
你写的 C 代码完全感知不到操作系统在背后复制内存、修改页表。
4. 高频考点汇总(背诵版)
- C 语言指针、取地址
&得到的地址,全部是虚拟地址,不是物理地址。 - fork 创建子进程,默认共享物理内存;只有写操作才触发写时拷贝。只读不拷贝。
- 写时拷贝:读共享,写复制。目的:加快 fork 速度,避免没必要的内存拷贝。
- 每个进程:独立 task_struct、独立虚拟地址空间、独立页表。
- 虚拟地址只是进程自己的编号,不同进程相同虚拟地址,可以映射到完全不同的物理内存。
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 写时拷贝串起来)
- 32 位机器,有 0~4G 的一套地址编号(虚拟地址)。
- 操作系统为每个进程创建
mm_struct,把 4G 虚拟地址切分成:代码段、只读段、全局数据、堆、栈。 - C 语言代码直接使用这套编号(
&变量拿到的就是这个编号 = 虚拟地址)。 - 虚拟地址不是真实内存条地址,需要页表映射到物理内存。
- fork 创建子进程:复制父进程的
mm_struct、复制页表,但是物理内存只读共享;写的时候触发写时拷贝。
五、考点背诵版
- 32 位地址总线:地址总数 (2^{32}),对应 4GB 地址空间。
- 地址空间本质:地址编号的连续范围,靠 start、end 划分不同区域。
- Linux 内核中,
struct mm_struct描述一个进程的虚拟地址空间,存放各个段起止地址。 task_struct(PCB)里面包含mm_struct,PCB 是进程总的描述,mm_struct 专门管内存地址空间。- 堆扩大 / 缩小,本质修改
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),进程本身代码完全不用修改。
物理内存不够的时候,可以把暂时不用的内存页面挪到硬盘,进程感知不到。
四、整张图逻辑串起来
- OS 给每一个进程,独立分配一套虚拟地址空间(大饼)。
- 每个进程都认为自己独占全部内存,互相隔离,看不到彼此。
- 进程访问内存时,虚拟地址通过页表翻译成物理地址。
- 翻译的过程中做权限检查,非法访问直接拦截,实现内存保护。
- 内核靠
task_struct、mm_struct、页表这一套结构体管理进程和它的虚拟内存。
考点背诵精简版
-
进程 = 内核数据结构(task_struct、mm_struct、页表)+ 代码与数据。
-
虚拟地址空间:给每个进程虚构一套完整连续地址,每个进程独立,互相隔离。
-
三大意义:
① 统一内存视角,简化程序编写;
② 地址转换时做访问检查,实现内存保护;
③ 解耦进程管理和内存管理。
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 读写)
示例表格:
表格
| 虚拟地址 | 物理地址 | 权限 |
|---|---|---|
| 0x123456 | 0x321 | rw |
| 0x1111 | 0x12 | r |
举例子:CPU 访问虚拟地址0x1111
- CPU 拿 cr3 找到当前进程页表
- 查询页表:虚拟 0x1111 → 物理地址 0x12,权限是 r(只读)
- 如果代码尝试对这个地址写数据,权限校验失败,直接报错段错误。
代码段、字符常量区标记为只读,就是在页表里设置权限位,防止程序误修改代码。
代码段(正文段 .text)
存放:编译好的机器指令(二进制 CPU 指令,就是程序要执行的代码)
比如你写的 C 语言printf、if、循环,编译后变成一条条机器指令,全部放在这里。
✅ 权限:只读
字符常量区(.rodata,只读数据段)
存放:字符串常量、const 修饰的全局常量
例:char *p = "hello";,"hello"这个字符串就放在这里。
✅ 权限:只读
为什么这两块区域设置成只读(考点)
-
保护指令,防止被篡改
程序运行的机器指令不能随便修改。如果代码段可写,程序跑着跑着不小心改写指令,直接崩溃,甚至被恶意代码攻击。
-
支持内存共享,节省物理内存
多个进程同时运行同一个程序(比如开多个 bash 窗口),它们的代码是一模一样的。
操作系统可以让多个进程的虚拟地址,映射到同一份物理内存里的代码。
因为标记为只读,大家只能读,谁都改不了,共享就安全。
举例子:打开 3 个 xshell,运行 3 份 bash 进程。
3 个 bash 进程,共享同一份物理内存里的 bash 代码,不用在内存存 3 份代码,节约内存。
-
常量本身就不允许修改
"hello"字符串常量,语法上就不允许修改。放到只读区域,一旦尝试写,硬件 + 页表直接拦截,触发段错误。
3. 内核空间放在最高地址
内核是操作系统本身,所有进程共享同一份内核虚拟地址。
放到整个 4G 地址的最顶端,直接和用户空间划一条分界线。
用户进程不能随便访问内核空间,硬件 + 页表做权限拦截。
4. 惰性加载(懒加载 + 缺页中断,核心考点)
共识:现代操作系统,几乎不做任何浪费空间和浪费时间的事情
问题:创建进程的时候,先建内核结构体,还是先把磁盘可执行程序全部加载进内存?
✅ 先创建内核数据结构(task_struct、mm_struct、页表),不会一次性把整个程序全部读到内存!
一次性把大程序全部载入内存很慢、浪费内存。操作系统用惰性加载。
流程:
-
刚创建进程:只建立虚拟地址空间、页表。代码数据还留在磁盘,没有放进物理内存。
-
进程运行,访问某一块虚拟地址。
-
CPU 查页表,发现:这块虚拟地址对应的物理内存不存在 → 触发缺页中断。
-
进入内核的缺页中断处理函数:
- 操作系统去磁盘,把需要的那一页代码,加载到物理内存
- 修改页表,填上物理地址,标记有效
- 返回用户态,重新执行刚才出错的指令。
一句话:用到哪一页,才加载哪一页,不用的就留在磁盘。
好处:大文件分批加载,节省内存,进程启动速度更快。
图上问题:你怎么知道进程的代码数据在不在内存?
答:看页表里面的有效位(存在位),有效位 = 0 → 不在内存,触发缺页中断。
5. 物理内存 ↔ 磁盘
物理内存容量有限,操作系统还可以把暂时不用的页面,换出到磁盘 swap 分区;等需要的时候再换回内存。进程完全感知不到。
6. 全部考点汇总(背诵版)
- cr3 寄存器:存放当前进程页表物理地址;进程切换时修改 cr3,实现地址空间切换。
- 页表两个作用:①虚拟地址转物理地址;②权限检查(r/rw),实现内存保护。
- 代码段、常量区在页表设置只读权限,保护指令,支持多进程共享代码。
- 创建进程:先创建内核结构体,不一次性加载全部程序到内存。
- 惰性加载:按需加载,访问页面时,缺页中断把磁盘数据载入内存。
- 缺页中断:访问虚拟地址,页表发现页面不在物理内存,触发中断,由 OS 加载页面。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)