进程切换和线程切换

 

为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换(process switch)、任务切换(task switch)或上下文切换(content switch)。

原文 :https://www.cnblogs.com/kkshaq/p/4547725.html

进程切换分两步:

1.切换页目录以使用新的地址空间

2.切换内核栈和硬件上下文

对于linux来说,线程和进程的最大区别就在于地址空间,对于线程切换,第1步是不需要做的,第2是进程和线程切换都要做的。

切换的性能消耗:

1、线程上下文切换和进程上下问切换一个最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。

2、另外一个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制。简单的说,一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废了。还有一个显著的区别是当你改变虚拟内存空间的时候,处理的页表缓冲(processor’s Translation Lookaside Buffer (TLB))会被全部刷新,这将导致内存的访问在一段时间内相当的低效。但是在线程的切换中,不会出现这个问题。

 

系统调用:处于进程上下文

系统调用是在进程上下文中,并没有tasklet之类的延迟运行,系统调用本身可以休眠,这些可以参见内核代码
 

虽然系统调用实与其他中断实现有点类似,通过IDT表查找入口处理函数,但是系统调用与其他中断最大的不同是,系统调用是代表当前进程执行的,所以current宏/task_struct是有意义的,这个休眠可以被唤醒

 

系统调用,异常,中断(其中中断是异步时钟,异常时同步时钟),也可以把系统调用成为异常

中断上下文:在中断中执行时依赖的环境,就是中断上下文(不包括系统调用,是硬件中断)

进程上下文:当一个进程在执行时,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容被称为该进程的上下文

1、首先,这两个上下文都处于内核空间。

2、其次,两者的区别在于,进程上下文与当前执行进程密切相关,而中断上下文在逻辑上与进程没有关系。

进程上下文主要是异常处理程序和内核线程。内核之所以进入进程上下文是因为进程自身的一些工作需要在内核中做。例如,系统调用是为当前进程服务的,异常通常是处理进程导致的错误状态等。所以在进程上下文中引用current是有意义的。

内核进入中断上下文是因为中断信号而导致的中断处理或软中断。而中断信号的发生是随机的,中断处理程序及软中断并不能事先预测发生中断时当前运行的是哪个进程,所以在中断上下文中引用current是可以的,但没有意义。事实上,对于A进程希望等待的中断信号,可能在B进程执行期间发生。例如,A进程启动写磁盘操作,A进程睡眠后现在时B进程在运行,当磁盘写完后磁盘中断信号打断的是B进程,在中断处理时会唤醒A进程。
上下文这个词会让人想到进程的CPU寄存器状态,但好像进入进程上下文(异常处理系统调用)和进入中断上下文(中断处理),内核所做的工作没有太大区别。所以,这两个上下文的主要区别,我认为在于是否与进程相关。

运行于进程上下文的内核代码是可抢占的,但中断上下文则会一直运行至结束,不会被抢占。因此,内核会限制中断上下文的工作,不允许其执行如下操作:

(1) 进入睡眠状态或主动放弃CPU;

由于中断上下文不属于任何进程,它与current没有任何关系(尽管此时current指向被中断的进程),所以中断上下文一旦睡眠或者放弃CPU,将无法被唤醒。所以也叫原子上下文(atomic context)。

  (2) 占用互斥体;

为了保护中断句柄临界区资源,不能使用mutexes。如果获得不到信号量,代码就会睡眠,会产生和上面相同的情况,如果必须使用锁,则使用spinlock。

  (3) 执行耗时的任务;

中断处理应该尽可能快,因为内核要响应大量服务和请求,中断上下文占用CPU时间太长会严重影响系统功能。在中断处理例程中执行耗时任务时,应该交由中断处理例程底半部来处理。

  (4) 访问用户空间虚拟内存。

因为中断上下文是和特定进程无关的,它是内核代表硬件运行在内核空间,所以在中断上下文无法访问用户空间的虚拟地址

   (5) 中断处理例程不应该设置成reentrant(可被并行或递归调用的例程)。

因为中断发生时,preempt和irq都被disable,直到中断返回。所以中断上下文和进程上下文不一样,中断处理例程的不同实例,是不允许在SMP上并发运行的。

  (6)中断处理例程可以被更高级别的IRQ中断。(不能嵌套中断)使用软中断,上部分关中断,也就是禁止嵌套,下半部分使用软中断

如果想禁止这种中断,可以将中断处理例程定义成快速处理例程,相当于告诉CPU,该例程运行时,禁止本地CPU上所有中断请求。这直接导致的结果是,由于其他中断被延迟响应,系统性能下降。

 软中断是一种延时机制,代码执行的优先级比进程要高,比硬中断要低。相比于硬件中断,软中段是在开中断的环境中执行的(长时间关中断对系统的开销太大), 代码是执行在中断/线程上下文的,是不能睡眠的,虽然每个cpu都有一个对应的ksoftirqd/n线程来执行软中断,但是do_softirq这个函数也还会在中断退出时调用到,因此不能睡眠(中断上下文不能睡眠的原因是由于调度系统是以进程为基本单位的,调度时会把当前进程的上下文保存在task_struct这个数据结构中,当进程被调度重新执行时会找到执行的断点,但是中断上下文是没有特定task_struct结构体的,当然现在有所谓的线程话中断,可以满足在中断处理函数执行阻塞操作,但是实时性可能会有问题。还有就是中断代表当前进程执行的概念,个人感觉有点扯淡,毕竟整个内核空间是由所有进程共享的,不存在代表的概念)

上面我们介绍的可延迟函数运行在中断上下文中(软中断的一个检查点就是do_IRQ退出的时候),于是导致了一些问题:软中断不能睡眠、不能阻塞。由于中断上下文出于内核态,没有进程切换,所以如果软中断一旦睡眠或者阻塞,将无法退出这种状态,导致内核会整个僵死。但可阻塞函数不能用在中断上下文中实现,必须要运行在进程上下文中,例如访问磁盘数据块的函数。因此,可阻塞函数不能用软中断来实现。但是它们往往又具有可延迟的特性。

<think>我们正在讨论进程切换线程切换的协作机制。根据引用[1],进程切换线程切换的主要区别在于进程切换涉及虚拟地址空间的切换,而线程切换不涉及,因为线程共享进程的虚拟地址空间。引用[3]提到线程切换的开销远小于进程切换,因为线程切换只需保存设置少量寄存器内容,而进程切换需要保存整个CPU环境并可能涉及内存空间I/O设备的分配或回收。 现在,用户想理解进程切换线程切换如何协同工作。我们需要从操作系统的角度解释这两种切换在同一个系统中的协作机制。 ### 分析 1. **进程切换**:当操作系统决定从进程A切换进程B时,需要: - 保存进程A的上下文(包括CPU寄存器、程序计数器等) - 切换虚拟地址空间(即切换页表) - 恢复进程B的上下文 这个过程涉及较高的开销(引用[3])。 2. **线程切换**:当在同一个进程内从一个线程切换到另一个线程时: - 不需要切换虚拟地址空间(因为线程共享进程的地址空间) - 只需保存恢复线程的私有上下文(如栈指针、程序计数器等寄存器) 因此开销较小(引用[1][3])。 3. **协作机制**:在操作系统中,线程切换可能发生在进程内部,而进程切换则是跨进程的。调度器(scheduler)负责管理这些切换。当调度器决定切换执行流时: - 如果切换发生在同一进程内的线程之间,则执行线程切换(轻量级)。 - 如果切换发生在不同进程之间(或者同一进程内的线程切换到另一个进程线程),则必须先进行进程切换切换地址空间),然后在该进程内部再进行线程切换。 ### 详细步骤 考虑以下场景:系统中有两个进程P1P2,其中P1有两个线程T1T2,P2有一个线程T3。调度器需要从P1的T1切换到P2的T3。 1. **中断当前线程**:无论何种切换,首先由中断(时钟中断、系统调用等)触发,CPU进入内核态。 2. **保存当前线程上下文**:保存T1的寄存器状态(包括程序计数器、栈指针等)到T1的线程控制块(TCB)中。 3. **判断切换类型**:调度器发现接下来要运行的是另一个进程(P2)中的线程(T3),因此需要进程切换。 4. **进程切换**: - 保存P1的进程上下文(包括文件描述符表、信号处理等)到进程控制块(PCB)。 - 切换页表:将CR3寄存器(x86架构)更新为P2的页表基址(切换虚拟地址空间)[^2]。 - 切换内核栈:因为每个进程有独立的内核栈(用于系统调用中断处理)。 5. **线程切换**:在P2的上下文中,调度器选择T3运行: - 从T3的TCB中恢复其寄存器上下文。 - 设置栈指针为T3的用户栈指针。 6. **返回用户态**:使用恢复的上下文(包括程序计数器)返回到T3的执行点。 ### 协作流程图 ```mermaid sequenceDiagram participant CPU participant 调度器 participant 内存管理单元(MMU) CPU->>调度器: 中断/系统调用发生 调度器->>调度器: 保存当前线程(T1)上下文 调度器->>调度器: 确定下一个执行线程(T3) 调度器->>调度器: 检查T3所属进程(P2) alt 同一进程 调度器->>CPU: 直接恢复T3上下文 else 不同进程 调度器->>调度器: 保存P1进程上下文 调度器->>MMU: 切换页表(P2) MMU-->>调度器: 页表切换完成 调度器->>调度器: 切换内核栈(P2) end 调度器->>调度器: 恢复T3线程上下文 调度器->>CPU: 返回用户态执行T3 ``` ### 关键点总结 1. **切换层级**:线程切换进程内部的操作,而进程切换是系统级的操作。进程切换必然伴随着至少一次线程切换(因为每个进程至少有一个线程)。 2. **开销差异**:进程切换需要刷新TLB(由于页表切换),而线程切换则不需要(引用[1][2])。 3. **协同方式**:操作系统调度器统一管理进程线程。当需要切换执行流时: - 如果目标线程属于同一进程,则仅执行线程切换。 - 如果目标线程属于不同进程,则先执行进程切换(包括地址空间切换),再执行线程切换。 ### 性能影响 根据引用[3]: - 线程切换开销:主要涉及寄存器保存/恢复(约几百纳秒)。 - 进程切换开销:包括寄存器操作、TLB刷新、内核栈切换等(约几微秒到几十微秒)。 因此,在设计并发程序时,应优先考虑多线程(而非多进程)以减少上下文切换开销。 --- ### 总结 进程切换线程切换的协作机制可归纳为: 1. **线程切换进程内操作**:不涉及地址空间变更,仅需保存/恢复线程私有状态[^1]。 2. **进程切换是跨进程操作**:必须切换地址空间内核资源,随后在目标进程内执行线程切换[^2]。 3. **调度器统一决策**:根据目标线程所属进程决定是否触发进程切换,形成两级切换机制。 $$ \text{总切换开销} = \begin{cases} \text{线程切换开销} & \text{同进程} \\ \text{进程切换开销} + \text{线程切换开销} & \text{跨进程} \end{cases} $$ §§相关问题§§ 1. 多核处理器中进程/线程切换有哪些额外考虑? 2. 如何通过编程模型减少上下文切换开销? 3. 操作系统如何优化跨进程线程切换的性能? 4. 虚拟化环境下进程切换有何不同?</think>### 进程切换线程切换的协作机制 #### 一、核心协作原理 1. **分层调度架构** - **进程切换**:由内核直接控制,涉及虚拟地址空间切换(页表更新)[^1] - **线程切换**:在进程内部由线程库管理,内核仅感知轻量级进程(LWP)切换 $$ \text{总切换} = \begin{cases} \text{进程切换} & \text{(地址空间切换)} \\ \text{线程切换} & \text{(寄存器组切换)} \end{cases} $$ 2. **协作触发场景** ```mermaid graph TD A[时钟中断] --> B{切换决策} B -->|同进程| C[线程切换] B -->|跨进程| D[进程切换] D --> E[线程切换] ``` #### 二、详细协作流程 1. **跨进程线程切换**(需进程切换协作) ```mermaid sequenceDiagram participant CPU participant 内核调度器 participant 线程库 CPU->>内核调度器: 时钟中断(时间片耗尽) 内核调度器->>内核调度器: 保存当前进程上下文 内核调度器->>内核调度器: 切换页表(CR3寄存器更新)[^1][^2] 内核调度器->>内核调度器: 加载目标进程上下文 内核调度器->>线程库: 移交执行权 线程库->>线程库: 选择目标线程(用户态) 线程库->>CPU: 恢复线程寄存器组 ``` - **关键步骤**: - 页表切换耗时占比 ≈70% (TLB刷新)[^2] - 寄存器恢复耗时 ≈100ns 2. **同进程线程切换**(纯线程切换) ```c // 伪代码:用户态切换 void __switch_thread(TCB* next) { asm volatile( "pushf\n" // 保存标志寄存器 "push %%rbp\n" // 保存栈基址 "mov %%rsp, %0\n" // 保存当前栈指针 "mov %1, %%rsp\n" // 加载新栈指针 "pop %%rbp\n" // 恢复新线程基址 "popf\n" // 恢复标志位 : "=m"(current->rsp) : "m"(next->rsp) ); } ``` - 无内核介入,直接操作寄存器[^3] - 典型耗时:200-500ns #### 三、性能优化机制 1. **惰性 TLB 刷新** - 进程切换时不立即刷新TLB - 标记为"无效",访问时触发缺页异常 - 减少 40% 跨进程切换开销[^2] 2. **线程缓存亲性** ```python # Linux cgroups 示例 echo $PID > /sys/fs/cgroup/cpuset/app/tasks # 绑定进程到CPU核 for thread in $THREADS; do taskset -cp $CORE $thread # 绑定线程到同核 done ``` - 同核线程切换免缓存失效 - 跨核切换增加 15-20% 延迟 3. **混合调度策略** | 调度场景 | 切换类型 | 典型延迟 | 优化手段 | |------------------|---------------|-----------|------------------| | 同进程同核线程 | 纯用户态切换 | 200ns | 无锁任务队列 | | 同进程跨核线程 | 轻内核介入 | 800ns | CPU亲性绑定 | | 跨进程线程 | 完整进程切换 | 3-5μs | 惰性TLB/SMP优化 | #### 四、资源管理协作 1. **内存空间协作** ```mermaid graph LR A[进程虚拟空间] --> B[线程1用户栈] A --> C[线程2用户栈] A --> D[共享堆/数据段] E[内核空间] --> F[进程页表] E --> G[线程1内核栈] E --> H[线程2内核栈] ``` - 线程共享进程页表(无切换开销)[^1] - 各线程独占用户栈+内核栈 2. **信号处理协作** - 信号发送到进程:由主线程处理 - 信号发送到线程:目标线程直接响应 - 信号掩码线程级独立存储 #### 五、现代操作系统实现 1. **Linux 调度器(CFS)协作** ```c // 内核调度流程简化 void schedule() { struct task_struct *prev = current; struct task_struct *next = pick_next_task(); // 选择任务 if (prev->mm != next->mm) { // 跨进程判断 switch_mm(prev->mm, next->mm); // 页表切换 } context_switch(prev, next); // 寄存器切换 } ``` - `pick_next_task()` 可能选择同进程线程 - 仅跨进程时调用 `switch_mm()`[^2] 2. **Windows 线程代理机制** - 用户态调度器实现线程切换 - 内核仅感知进程CPU时间配额 - 减少 60% 内核陷入开销 ### 关键结论 1. **切换开销本质** $$ \text{总开销} = \overbrace{\tau_{\text{地址空间}}}^{\text{进程切换}} + \underbrace{\tau_{\text{寄存器}}}_{\text{线程切换}} $$ - 地址空间切换:仅跨进程时发生[^1] - 寄存器切换:始终发生 2. **性能对比** | 切换类型 | 关键操作 | 典型延迟 | 共享资源 | |---------------|--------------------------|----------|------------------| | **进程切换** | 页表切换+寄存器保存 | 3-5μs | 无 | | **线程切换** | 寄存器保存 | 0.2-0.8μs| 虚拟内存/文件描述符[^3] | 3. **设计启示** - I/O密集型应用:多进程隔离故障 - 计算密集型应用:多线程减少切换开销 - 混合架构:进程为容器,线程为执行单元 --- ### 总结 进程切换线程切换通过**分层协作机制**实现高效协同: 1. **内核层**:管理进程级资源(虚拟内存、文件描述符表),仅在跨进程时执行昂贵的页表切换[^1][^2] 2. **用户层**:线程库管理线程寄存器状态,实现纳秒级快速切换[^3] 3. **硬件层**:CPU提供快速上下文切换指令(如`fxsave`/`fxrstor`) 这种协作使操作系统既能保障进程隔离性,又能通过线程实现轻量级并发,满足不同应用场景的性能需求[^1][^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值