CPU与进程的执行
前置知识
寄存器(register)
CPU 芯片内部自带的一排”随身小抽屉”——数量极少(十几个)、速度极快的存储格。CPU 做任何运算,数据都得先搬进寄存器才能动手。寄存器快到什么程度?访问一次寄存器约 1 个时钟周期,而访问内存要 100 多个周期,差了两个数量级。
程序计数器 PC(Program Counter)
一个专门记”下一条要执行的指令在哪”的寄存器。它永远指向”接下来该执行哪条指令的地址”,CPU 每取完一条指令,PC 就自动往前挪,指向下一条。没有它,CPU 就不知道该干到哪了。
指令寄存器 IR(Instruction Register)
CPU 从内存里刚取出来的那条指令,先临时放在这里,等译码器来读它。
时钟周期 / 主频(clock / frequency)
CPU 内部有一个”节拍器”(时钟),每敲一下,就推动芯片各部件往前走一步。这个节拍的频率就是主频,单位 Hz(赫兹)。3.5 GHz 就是每秒敲 35 亿下。所有部件都靠这个统一节拍协同工作。
缓存(cache)
CPU 和内存之间的一层”中转加速器”,因为内存太慢,CPU 等不起。缓存把”最近/最可能用到的数据”提前搬到离 CPU 更近、更快的地方。它分三级:
- L1:最快、最小(32~64KB),还分成”指令缓存”和”数据缓存”两半;
- L2:次快(256KB~1MB);
- L3:最大(几 MB~几十 MB),多个核心共享。
局部性原理(locality)——缓存为什么有用的根因
缓存不是瞎存的,它赌的是局部性这条规律:程序倾向于反复访问同一小片数据/指令。分两种:
- 时间局部性:刚用过的,很快还会再用(循环里那条指令被执行一万遍);
- 空间局部性:用到一个地址,它旁边的也快被用了(遍历数组,读完 a[0] 读 a[1])。
统计上”程序 90% 时间跑在 10% 的代码上”。所以缓存命中率能到 95% 以上。
进程(process)
程序是磁盘上那个静态文件;进程是程序被跑起来的那一次执行,是活的、动态的。同一个程序跑两次,就是两个进程。
进程状态(三态 / 五态)
进程在生命周期里会在几个状态之间跳来跳去,最基本的三个:
- 就绪态:万事俱备,只等 CPU(在排队);
- 运行态:正占着 CPU 干活;
- 阻塞态:干不下去,在等某件事(比如等键盘输入、等磁盘读完),把 CPU 让给别人。
还有两个补充状态(凑成五态):新建态(刚创建、还没就绪)、终止态(执行完毕)。
上下文切换(context switch)
CPU 只有一个,进程有几十上百个,怎么做到”看起来都在同时跑”?靠快速轮流。每次从进程 A 换到进程 B,得先把 A 的”现场”保存下来(它的寄存器值、PC 值、栈指针……),再把 B 的现场恢复出来。这个”保存 A 现场 + 恢复 B 现场”的动作,就叫上下文切换。”上下文”就是”一个进程跑到现在这一刻的全部状态”。
时间片(time slice)
“轮流跑”的节奏单位。操作系统给每个进程一小段固定的 CPU 时间(比如几毫秒到几十毫秒),用完了就强制换下一个。这一小段就是时间片。
调度(scheduling)与调度器(scheduler)
操作系统里专门负责”决定下一个让谁上 CPU“的模块,叫调度器;它做决定这件事叫调度。怎么选、按什么规则选,就是调度算法。
系统调用 / 内核态与用户态
- 用户态:普通程序运行时处于的”受限模式”,只能碰自己的内存,很多危险操作(直接操作硬件)不让做。
- 内核态:操作系统的核心代码运行时的”特权模式”,什么都能干。
- 系统调用:程序从用户态”请求”进入内核态去办大事的合法入口(比如读写文件、创建进程)。
CPU怎么执行指令
指令周期
取指(Fetch)
CPU 看 PC 指到哪,去那里把下一条指令从内存(实际大多从缓存)读出来,放进 IR;同时 PC 自动加一,指向下一条。
译码(Decode)
译码器读 IR 里的二进制指令,搞清楚”这条是要干嘛?用什么数据?结果放哪?”。机器码是一串人看不懂的 0/1,这一步把它翻译成”行动指令”。
执行(Execute)
真正动手——算术逻辑单元(ALU)做加减乘除、逻辑运算,或者算出一个内存地址。
访存(Memory Access)
如果这条指令要读写内存(比如”把某地址的值取来”),就在这一步去访问内存。
写回(Write Back)
把结果存回寄存器或内存,并更新一些”状态标志”(比如结果是正、是零、还是溢出)。
然后 PC 已经指向下一条,回到 ① 循环,每秒重复几十亿次。
为什么处理的快
流水线
把指令周期拆成好几个阶段,像工厂流水线——第 1 条在执行时,第 2 条在译码、第 3 条在取指,同时进行。
超标量
干脆开多条流水线,一个周期同时发射好几条指令。
乱序执行
有些指令互相依赖(要等结果),有些独立。CPU 会跳过正在等待的指令,先执行后面不依赖它的指令,最后再把结果按正确顺序”归位”。
分支预测
遇到 if、循环这种”要跳转”的指令,CPU 得猜”跳还是不跳”,提前把猜的那条路取进来。猜对了省时间,猜错了就”清空流水线”重来。
进程怎么被“轮流”执行
进程状态机:在三个态之间跳
看它活着时的状态变化。把三态串成一张状态机:
- 就绪 → 运行:调度器选中它,给它 CPU(被动);
- 运行 → 就绪:时间片用完了,或被更高优先级进程抢了 CPU(被动让出);
- 运行 → 阻塞:自己主动等 I/O、等锁,把 CPU 让出来(主动让出);
- 阻塞 → 就绪:等的资源到了,被操作系统唤醒,重新排队(被动)。
关键要记住一个区别:“运行→就绪”是被抢走的(被动),”运行→阻塞”是自己让出来的(主动)。
上下文切换:为什么能”同时跑很多程序”
CPU 就一个(先不考虑多核),几十个进程却像同时在跑,本质就是高频地切换 + 每个进程切一小片。
切换时要做的事:
- 保存当前进程的”现场”:所有寄存器值、PC(执行到哪了)、栈指针等;
- 恢复下一个进程之前保存的”现场”;
- 让 CPU 从下一个进程上次停的地方接着跑。
每个进程都以为自己”独占 CPU、连续地跑”,其实是被切成无数小片轮着跑的。
为什么切换有成本:切换本身不免费。要花时间保存/恢复寄存器、PC、内核栈;更贵的是进程级切换要换页表,导致 TLB(一种地址翻译的缓存)失效、缓存命中率下降。所以时间片不能切太小,否则光切换的开销就吃掉大半 CPU。这就是”时间片大小的权衡”:太大 → 响应慢(一个程序霸着 CPU);太小 → 切换开销爆炸。
调度算法家族
调度器怎么选”下一个让谁上”?常见的五种:
- 先来先服务 FCFS:谁先来谁先上,简单公平,但短任务会被前面长任务堵很久;
- 短作业优先 SJF:谁短先做谁,平均等待最短,但长任务可能一直饿死(饥饿);
- 优先级调度:按优先级,高的先跑;低优先级可能饿死(可用”老化”补救——等越久优先级越高);
- 时间片轮转 RR:排成队,每人轮一小片,兼顾公平和响应,适合分时系统;
- 多级反馈队列 MLFQ:综合方案,多个优先级队列,兼顾交互响应和吞吐,是主流 OS 的思路(Linux 的 CFS 调度器就是在”按虚拟运行时间公平调度”上做的实现)。
像食堂窗口——FCFS 是”老实排队”;SJF 是”谁吃得快谁先来”;优先级是”VIP 插队”;RR 是”每人限时 5 分钟”;MLFQ 是”新来的先给一小段试试,表现好的往上提”。
这节工具
1. lscpu——看 CPU 的”身份证”
1 | |
输出你的 CPU 架构(x86-64 还是 ARM)、核心数、线程数、缓存大小(L1/L2/L3)、主频。这节讲的”架构、寄存器位数、缓存”全都能在这条命令里对上号。
2. top——实时看进程”抢 CPU”
1 | |
实时显示每个进程的 CPU 占用率、内存占用、状态(R=运行、S=睡眠/阻塞)。这节讲的”调度、时间片、进程状态”在这里是活的——可以看到 CPU 占用率在进程之间不停地跳。
3. gdb 的 info registers——看寄存器现场
1 | |
打印所有寄存器的当前值(RAX、RBX、RSP、RBP、RIP……)。这节讲的”寄存器、PC、上下文”在这里能看到——RIP 就是那个”程序计数器”,info registers 时它指向的就是下一条要执行的指令地址。