Linux - 进程切换
🎁个人主页:工藤新一¹
🔍系列专栏:C++面向对象(类和对象篇)
🌟心中的天空之城,终会照亮我前方的路
🎉欢迎大家点赞👍评论📝收藏⭐文章
文章目录
- 进程切换 && 进程调渡
- 一、补充概念-竞争、独立、并行、并发
- 1.1 独立(Independent)
- 1.2 并发(Concurrent)
- 1.3 并行(Parallel)
- 1.4并发 vs. 并行 的关键区别
- 1.5 竞争(Race Condition)
- 1.6 关系总结与图示
- 二、进程切换
- 2.1 什么是进程切换?如何理解?
- **A. 死循环如何运行?**
- **B. CPU,寄存器**
- 2.2 进程切换如何来完成?
进程切换 && 进程调渡
一、补充概念-竞争、独立、并行、并发
1.1 独立(Independent)
- 定义:指多个任务(进程或线程)之间没有任何关联,互不影响。一个任务的执行既不依赖另一个任务的输出,也不会共享任何资源(如数据、文件、内存等)。
- 核心:无共享,无交互。
- Linux 示例:
- 你电脑上的文本编辑器进程和后台播放音乐的进程,通常是独立的。它们各自处理自己的任务,一个崩溃了通常不会直接影响另一个。
- 两个毫不相干的 shell 命令,例如
ls /home
和date
,它们就是独立的。
- 比喻:两条平行的铁轨上行驶的火车,永不相交,互不干扰。
1.2 并发(Concurrent)
- 定义:指系统具有处理多个任务的能力。这些任务在时间上是重叠的,即在一个时间段内,有多个任务都在推进。它不指定这些任务是否同时运行。
- 核心:重叠的时间段,有能力处理多任务。这是逻辑上的概念。
- 与硬件的联系:并发可以在单核 CPU 上实现。操作系统通过快速的时间片轮转,在极短的时间间隔内切换执行不同的任务,从而让用户感觉所有任务在同时进行。
- Linux 示例:
- 在单核 CPU 的电脑上,你一边用浏览器上网,一边用播放器听音乐。CPU 快速地在两个进程间切换,宏观上看起来它们是同时运行的,这就是并发。
- 一个单核服务器同时处理多个来自客户端的网络请求。
- 比喻:一个厨师同时照看一口锅里炖的汤、一口锅里炒的菜。他需要交替进行(搅拌汤、翻炒菜),在一顿饭的时间段内,两件事都在推进。
1.3 并行(Parallel)
- 定义:指系统同时执行多个任务。在同一个时刻,有多个任务真正地在同时运行。
- 核心:同一时刻,真正同时执行。这是物理上的概念。
- 与硬件的联系:并行必须在多核 CPU、多个 CPU 或多台机器上才能实现。每个核心可以独立执行一个任务。
- Linux 示例:
- 在多核 CPU 上,内核可以将不同的进程或线程调度到不同的核心上真正同时运行。
- 使用
make -j4
编译大型项目,-j4
选项告诉make
工具启动 4 个编译任务,它们可以在多个核心上并行运行,极大加快编译速度。
- 比喻:多位厨师在同一厨房的不同灶台上同时做不同的菜。
1.4并发 vs. 并行 的关键区别
这是一个非常经典的面试题。
- 并发是关于代码结构的,并行是关于执行的。
- 并发是问题,并行是解决方案。我们编写并发程序(如多线程程序)来解决问题,而硬件(多核CPU)提供并行执行的能力来加速这个并发程序。
- 并发是逻辑上的同时发生,并行是物理上的同时发生。
- 并发的程序不一定能并行,但能并行的程序一定是并发的。一个设计不良的并发程序可能因为资源竞争而无法有效利用多核。
一句话总结:并发是“同时”管理很多事情,并行是“同时”做很多事情;并行在任意 “时刻” 同时运行多个进程,并发在某 “时间段” 使多个进程同时推进
1.5 竞争(Race Condition)
-
定义:这是一种错误状态,指多个并发/并行执行的进程或线程访问和操作共享资源(如全局变量、文件、内存) 时,最终的结果依赖于它们执行的具体时序。由于时序是不可预测的,导致程序的行为变得不确定,可能产生非预期的、错误的结果。
-
核心:结果的不确定性。这是一个需要避免的 Bug。
-
产生条件:
- 存在共享资源(数据)。
- 存在多个执行流(并发/并行)访问该资源。
- 至少有一个执行流是写操作。
- 缺乏同步机制(如锁、信号量)来保护访问顺序。
-
Linux 示例:
// 全局共享变量 int counter = 0;// 线程函数 void *increment(void *arg) {for (int i = 0; i < 100000; i++) {counter++; // 这不是原子操作!}return NULL; }int main() {pthread_t thread1, thread2;pthread_create(&thread1, NULL, increment, NULL);pthread_create(&thread2, NULL, increment, NULL);pthread_join(thread1, NULL);pthread_join(thread2, NULL);printf("Final counter value: %d\n", counter); // 很可能不是 200000return 0; }
counter++
看起来是一条语句,但在汇编层面是“读-改-写”三步。两个线程可能同时读到了相同的值,然后加一,再写回,导致其中一次增加被覆盖。最终结果是一个随机数,这就是竞争。
1.6 关系总结与图示
特性 | 独立 | 并发 | 并行 | 竞争 |
---|---|---|---|---|
核心思想 | 互不干扰 | 交替推进**(时间段)** | 同时执行**(时间点)** | 时序错误 |
依赖硬件 | 无 | 单核即可 | 必须多核 | 无(但并行会加剧) |
共享资源 | 无 | 可能有 | 可能有 | 必须有 |
是否期望 | 是(简化设计) | 是(提高效率) | 是(提高性能) | 否(必须避免) |
它们之间的关系可以这样看:
- 你编写了一个 并发 的程序(多线程)来解决一个问题,期望它能利用多核特性 并行 执行以提高速度。
- 如果这些并发/并行的线程访问了 非独立 的(即共享的)数据,并且你没有做好同步保护,那么就会产生 竞争 条件,导致程序出错。
- 为了让并发程序正确运行,你必须通过同步机制(如互斥锁)将那些访问共享资源的 临界区 序列化,使其在逻辑上变回“独立”的片段,从而消除竞争。
• **竞争性:**系统进程数⽬众多,而 CPU资源只有少量,甚⾄1个,所以进程之间是具有竞争属性的。为 了⾼效完成任务,更合理竞争相关资源,便具有了优先级,权限…
• **独立性:**多进程运⾏,需要独享各种资源,多进程运⾏期间互不⼲扰
• **并行:**多个进程在多个 CPU下分别,同时进⾏运⾏,这称之为并⾏
• **并发:多个进程在单 CPU下采⽤“进程切换”**的方式,在⼀段时间之内,让多个进程都得以推进,这种模式称之为并发
二、进程切换
2.1 什么是进程切换?如何理解?
核心定义:
进程切换(Process Switching),也称为上下文切换(Context Switching),是指操作系统将CPU从一个正在运行的进程夺回,并分配给另一个就绪的进程,同时保存和恢复相应进程的状态的过程。
通俗理解:
想象一下你是一个学生**(CPU),正在做数学作业(进程A)。突然,你妈妈(OS)**说:“别做数学了,先来吃饭!(中断)” 于是你(CPU)需要:
- 停下来:放下数学笔。
- 做标记:用一个书签(保存上下文)精确地记录下你做到哪一题、哪一步,草稿纸上的关键数字是什么。(记录/保留当前痕迹)
- 换任务:起身去餐桌吃饭(进程B)。
- 吃完饭回来:你根据书签上的记录(恢复上下文),找到刚才的步骤和数字,继续做数学题。
这个过程就是“进程切换”。操作系统就是那个妈妈,它负责在你(CPU)不同的任务(进程)之间进行调度,而书签就是进程的上下文。(作业中的内容 — 进程运行时的临时数据,CPU寄存器中的内容)
为什么需要它?
- 实现多任务:让单个CPU也能“同时”运行多个程序(如边听歌边上网),靠的就是在进程间极速切换(每秒上百次),人类无法感知。
- 提高CPU利用率:当一个进程等待慢速操作时(如读磁盘、等网络),CPU不能闲着,立刻切换到其他可以运行的进程。
A. 死循环如何运行?
- a. 一旦进程占有CPU,会一直把自己的代码跑完吗?不会!
(除非进程的代码数据短)OS 会为每一个进程分配 时间片(给进程1ms时间让进程运行,若进程未运行结束,就将进程从 CPU上剥/抽离,使进程在运行队列中重新排队)。因此,任何进程对于 CPU资源的运用都是临时性的,这样就不会出现一个进程死占 CPU的情况
- b. 死循环进程不会打死系统,因为其不会一直都占有 CPU!
B. CPU,寄存器
2.2 进程切换如何来完成?
进程切换是操作系统内核最核心的工作之一,它完全在内核态下执行。整个过程可以分解为以下几个关键步骤,主要由 schedule()
函数实现:
触发时机(妈妈喊你吃饭的原因):
- 主动让出:进程自己调用
sleep()
,exit()
等系统调用。 - 时钟中断:一个硬件定时器每隔几毫秒发出一次中断,强制夺回CPU控制权,让调度器有机会决定是否切换。这是最常见的原因。
- 等待资源:进程需要等待磁盘I/O、网络包、锁等资源,自己进入阻塞状态。
- 被更高优先级进程抢占:一个更高优先级的进程变为就绪状态。
切换步骤(妈妈让你换任务的具体动作):
- 中断当前进程:时钟中断或系统调用触发,CPU硬件自动将用户态的上下文(如程序计数器PC、栈指针SP等)保存到当前进程的内核栈中,并切换到内核态。
- 执行内核中断处理程序:CPU开始执行操作系统预设的中断处理代码。
- 决定是否切换:中断处理程序调用调度器
schedule()
。调度器检查是否需要切换(例如当前进程的时间片用完了)。 - 保存当前进程上下文:如果需要切换,内核将当前进程的完整硬件上下文(所有CPU寄存器的值:通用寄存器、状态寄存器、程序计数器等)保存到它的进程控制块(PCB) 中。
- PCB(
task_struct
):Linux中每个进程都有一个巨大的数据结构(struct task_struct
),它是进程的“身份证”和“病历本”,记录了一切信息,其中就包括保存上下文的字段。
- PCB(
- 选择下一个进程:调度器从就绪队列中,根据某种算法(如CFS完全公平调度器)选出下一个最值得运行的进程。
- 切换地址空间:切换 CR3寄存器(在x86架构上)。这个寄存器指向当前进程的页目录,切换它就等于切换了整个虚拟内存空间。这是进程隔离的关键,进程A无法访问进程B的内存。
- 恢复下一个进程的上下文:内核将下一个进程的PCB中保存的寄存器值全部加载到CPU的各个寄存器中。这包括恢复它的程序计数器(PC),这意味着CPU接下来要执行的指令地址就变成了这个新进程的。
- 切换内核栈:将当前 CPU的栈指针(SP)指向新进程的内核栈。因为每个进程都有自己的内核栈,用于执行系统调用和中断处理。
- 返回用户态:上下文恢复完成后,中断处理程序执行返回指令。CPU硬件自动从新进程的内核栈中恢复用户态的上下文(PC, SP等),并切换到用户态。
- 开始运行新进程:由于PC寄存器指向的是新进程被中断时的指令地址,CPU便开始执行新进程,仿佛它从未停止过。
所以只要是在CPU内的寄存器(用户级别)我们可以观测到的寄存器都需要被保存
步骤 | 类比(学生做作业) | 技术对应 |
---|---|---|
触发 | 妈妈喊“先别做了,来吃饭” | 时钟中断或系统调用 |
保存现场 | 用书签标记做到哪一题、哪一步 | 将 CPU寄存器保存到旧进程的 PCB |
选择下一个 | 妈妈决定让你去吃饭 | 调度器从就绪队列选新进程 |
恢复现场 | 吃完饭回来,根据书签继续做题 | 将新进程 PCB 中的状态加载回 CPU寄存器 |
开始工作 | 继续演算数学题 | CPU从保存的程序计数器(PC) 地址开始执行 |
记住核心:进程切换的本质是 保存状态 -> 换人 -> (记录上一次痕迹)恢复状态。这个过程由硬件中断触发,由操作系统内核借助 进程控制块(PCB) 精密完成,是实现一切多任务魔力的基石
🌟 各位看官好,我是工藤新一¹呀~
🌈 愿各位心中所想,终有所致!