1-1-1-CPU调度进程与线程详解
1、多核CPU核心协作机制与进程/线程调度详解
多核CPU中每个核心都是单独工作的吗?之间不会存在同步机制吗?进程和线程在切换时分别是在处理什么?调度流程分别是什么?
一、多核CPU核心的工作模式与同步机制
1. 核心是否单独工作?
- 独立执行单元:每个核心拥有独立的ALU(算术逻辑单元)、寄存器组和指令流水线,可并行执行不同任务。
- 共享资源依赖:
- 内存与缓存:所有核心共享主存和L3缓存(部分架构L2也共享),需通过缓存一致性协议(如MESI)保证数据同步。
- 总线与I/O:通过前端总线(FSB)或片上网络(NoC)通信,可能因资源争用引发瓶颈。
2. 核心间同步机制
- 缓存一致性协议(MESI):
- 状态标记:每个缓存行标记为Modified(独占修改)、Exclusive(共享未改)、Shared(共享)、Invalid(无效)。
- 写传播与事务串行化:核心修改数据后,通过总线广播通知其他核心更新或失效缓存。
- 原子操作与锁:
- CAS(Compare-And-Swap):通过硬件指令实现无锁同步(如x86的
lock前缀指令)。 - 自旋锁/互斥锁:内核态通过信号量(Semaphore)或互斥量(Mutex)协调跨核心访问。
- CAS(Compare-And-Swap):通过硬件指令实现无锁同步(如x86的
- 核间通信(IPC):
- 共享内存:通过映射同一物理内存区域实现数据交换(需加锁)。
- 消息传递:使用队列或管道传递数据(如Linux的
mq_overview)。
二、进程与线程切换的底层处理
1. 进程切换(Context Switch)
- 触发场景:时间片耗尽、阻塞(I/O/锁)、高优先级进程抢占。
- 关键操作:
- 保存上下文:将当前进程的寄存器状态(包括PC、SP、通用寄存器)、页表基址、内核栈指针写入进程控制块(PCB)。
- 切换内存映射:更新CR3寄存器(x86)指向新进程的页表,使TLB(Translation Lookaside Buffer)失效。
- 恢复目标进程:从PCB加载上下文到CPU寄存器,跳转到程序计数器(PC)继续执行。
- 性能开销:约1-100μs,主要消耗在页表切换和TLB刷新。
2. 线程切换(Thread Switch)
- 触发场景:时间片耗尽、阻塞(I/O/锁)、主动让出(
pthread_yield)。 - 关键操作:
- 保存线程上下文:仅保存线程私有的寄存器状态(如x86的RAX-RSP)、栈指针和程序计数器。
- 无需切换内存映射:线程共享进程的页表,TLB无需刷新。
- 恢复线程执行:从线程控制块(TCB)加载上下文,继续执行。
- 性能开销:约1-10μs,比进程切换快10倍以上。
三、进程与线程的调度流程
1. 进程调度流程(以Linux CFS为例)
- 调度触发:
- 时钟中断(每1ms触发一次)检查时间片是否耗尽。
- 进程阻塞(如系统调用
read()等待I/O)或主动放弃CPU。
- 调度策略:
- CFS(完全公平调度器):基于虚拟运行时间(vruntime)选择最“饥饿”的进程。
- 实时优先级:高优先级任务(如硬中断处理)抢占低优先级进程。
- 调度步骤:
- 从就绪队列中选择vruntime最小的进程。
- 更新当前进程的vruntime(
vruntime += (NICE_0_LOAD * weight) / (current_weight))。 - 切换上下文到新进程。
2. 线程调度流程
- 用户态线程(如Go Goroutine):
- 调度器在用户态运行:通过抢占式调度(如Go的
sysmon协程)实现协作式切换。 - 无内核介入:切换成本低,但无法利用多核并行(需绑定到系统线程)。
- 调度器在用户态运行:通过抢占式调度(如Go的
- 内核态线程(如POSIX线程):
- 内核调度器管理:与进程调度流程类似,但上下文更轻量。
- M:N调度模型:将M个用户线程映射到N个内核线程(如Linux的
NPTL)。
四、多核场景下的调度优化
1. 负载均衡
- 任务窃取(Work Stealing):空闲核心从其他核心的任务队列尾部窃取任务(如Go调度器)。
- NUMA感知调度:优先将任务分配到本地内存节点,减少跨节点访问延迟。
2. 并行优化
- 锁粒度控制:减少锁竞争(如分段锁、无锁数据结构)。
- SIMD指令并行:利用AVX/AVX-512指令加速数据密集型任务。
3. 避免伪共享(False Sharing)
- 缓存行对齐:将热点变量填充到独立缓存行(如
struct{char data[64];})。 - 线程绑定(CPU Affinity):将线程固定到特定核心,减少缓存失效。
五、总结:多核协作的核心逻辑
- 独立与协作并存:核心独立执行指令,但通过共享内存和同步机制协同工作。
- 调度分层设计:操作系统内核管理进程/线程调度,硬件(如MESI协议)保障缓存一致性。
- 性能瓶颈突破:通过多核并行、NUMA优化、锁优化等手段提升吞吐量。
应用建议:
- 并行计算:使用OpenMP、MPI等框架显式分配任务到多核。
- 高并发服务:结合异步I/O(如epoll)与线程池,减少上下文切换开销。
2、进程时间片内线程切换的触发场景与流程详解
进程的一个时间片内还会触发线程切换?结合各个进程和线程切换的各个触发场景详细举例说明其流程
一、核心结论
是的,进程的一个时间片内可能触发线程切换。线程切换的触发与进程的时间片无关,而是由线程自身状态变化或调度策略决定。以下从触发场景、流程和示例三个维度展开分析。
二、触发线程切换的四大场景
1. 线程主动阻塞(同一进程内切换)
-
触发条件:线程执行I/O操作、等待锁、调用
sleep()等阻塞方法。 -
流程:
- 保存上下文:当前线程的寄存器、栈指针保存到内核栈。
- 状态变更:线程状态从
RUNNING变为BLOCKED,从运行队列移除。 - 调度决策:调度器选择同一进程的其他就绪线程(或不同进程的线程)执行。
-
示例:
// 线程A1执行I/O操作时阻塞 public void run() { readFromFile(); // 阻塞调用,触发线程切换 }
2. 时间片耗尽(同一进程内切换)
-
触发条件:线程的时间片用完(如10ms),调度器强制抢占。
-
流程:
- 保存上下文:当前线程的寄存器状态保存到内核栈。
- 时间片更新:线程的
vruntime(CFS调度器)或时间片计数器增加。 - 选择新线程:调度器从就绪队列选择下一个线程(可能为同一进程的其他线程)。
-
示例:
// Linux CFS调度器时间片耗尽 if (current->time_slice <= 0) { schedule(); // 触发线程切换 }
3. 高优先级线程抢占(跨进程切换)
-
触发条件:高优先级线程就绪,抢占低优先级线程。
-
流程:
- 抢占检查:调度器检测到更高优先级线程进入就绪队列。
- 强制切换:保存当前线程上下文,恢复高优先级线程上下文。
- 地址空间切换:若跨进程,更新CR3寄存器(Linux)或页表基址(Windows)。
-
示例:
// Windows实时优先级线程抢占 if (next_thread->priority > current_thread->priority) { KiSwapContext(current_thread, next_thread); }
4. 硬件中断或系统调用(跨进程/线程切换)
-
触发条件:硬件中断(如定时器、I/O完成)或系统调用返回。
-
流程:
- 中断处理:进入内核态执行中断服务例程(ISR)。
- 调度检查:中断处理完成后,调用
schedule()检查是否需要切换线程。 - 恢复执行:若需切换,恢复目标线程的寄存器和栈指针。
-
示例:
; 定时器中断处理(x86架构) timer_handler: pushad ; 保存通用寄存器 call do_timer ; 更新时间片计数器 popad ; 恢复寄存器 iret ; 返回用户态或触发调度
三、进程时间片与线程切换的关系
1. 时间片是进程调度的单位,线程切换是更细粒度的调度
- 进程时间片:操作系统分配给进程的CPU时间(如100ms),用于宏观调度。
- 线程时间片:进程内分配给线程的CPU时间(如10ms),由调度器动态调整。
- 关系:
- 进程时间片耗尽 → 进程被挂起,调度其他进程。
- 线程时间片耗尽 → 线程被挂起,调度同一进程的其他线程或不同进程线程。
2. 同一进程内线程切换的开销更低
-
无需切换地址空间:共享进程的页表,避免TLB刷新。
-
仅保存线程私有状态:寄存器、栈指针(约几十字节)。
-
示例对比:
操作 进程切换开销 线程切换开销 保存上下文 100-200 CPU cycles 10-20 CPU cycles 切换地址空间 是(CR3更新) 否 调度延迟 高(ms级) 低(μs级)
四、详细流程示例
场景1:同一进程内线程A1阻塞触发切换
- 初始状态:进程P的线程A1正在运行,时间片剩余5ms。
- 触发条件:A1调用
read()阻塞等待磁盘I/O。 - 调度流程:
- 内核保存A1的寄存器状态到其内核栈。
- 将A1状态标记为
BLOCKED,移出运行队列。 - 调度器选择同一进程的线程A2(就绪态)执行。
- 恢复执行:磁盘I/O完成后,A1被重新加入就绪队列,等待调度。
场景2:跨进程线程抢占(高优先级线程B就绪)
- 初始状态:进程P1的线程T1运行,时间片剩余8ms。
- 触发条件:进程P2的高优先级线程T2被唤醒(如定时器到期)。
- 调度流程:
- 内核保存T1的上下文到其PCB。
- 更新CR3寄存器(若跨进程),切换到P2的地址空间。
- 恢复T2的上下文,继续执行。
- 时间片调整:T2继承剩余时间片(8ms),若未耗尽则继续运行。
场景3:硬件中断触发线程切换
- 初始状态:线程T运行在CPU核心0,时间片剩余5ms。
- 触发条件:网卡接收到数据包,触发中断。
- 中断处理:
- 保存T的上下文到内核栈。
- 执行中断服务例程(ISR),将数据包存入缓冲区。
- 调度决策:ISR返回后,调用
schedule()检查就绪队列。- 若有更高优先级线程就绪,切换线程;否则恢复T执行。
五、总结:线程切换的通用规则
- 触发时机:
- 主动阻塞:线程自身进入等待状态。
- 被动抢占:时间片耗尽、高优先级线程就绪、中断发生。
- 切换成本:
- 同一进程内切换成本极低(仅寄存器/栈保存)。
- 跨进程切换成本高(需切换地址空间)。
- 调度目标:
- 公平性:时间片轮转确保所有线程获得CPU时间。
- 实时性:抢占式调度满足高优先级任务需求。
应用建议:
- 多线程编程中,优先使用非阻塞I/O(如
epoll)减少线程阻塞。 - 实时系统需结合优先级继承(Priority Inheritance)避免优先级反转。
3、多核CPU中进程跨核心调度的机制与场景分析
多核CPU其他核心上的某个等待时间片的进程可能被分配到另一个核心处理?
一、核心结论
是的,多核CPU中等待时间片的进程可能被调度到其他核心执行。这种调度行为由操作系统的负载均衡机制和进程状态动态决定,目的是优化资源利用率和减少核心空闲。以下从调度策略、触发条件和实际案例三个维度展开分析。
二、进程跨核心调度的触发条件
1. 负载均衡(Load Balancing)
- 触发场景:
- 某核心负载过高(如长时间运行计算密集型任务),而其他核心空闲或负载较低。
- 新进程创建或阻塞进程唤醒时,调度器选择负载较轻的核心分配任务。
- 调度策略:
- CFS(完全公平调度器):通过红黑树动态选择
vruntime最小的进程,并优先分配到空闲核心。 - 抢占式迁移:当核心A的负载超过阈值(如CPU利用率>80%),调度器将部分进程迁移到核心B。
- CFS(完全公平调度器):通过红黑树动态选择
2. 进程状态变更
- 唤醒进程:阻塞的进程(如I/O完成)被唤醒时,可能被分配到当前负载最低的核心。
- 新进程创建:默认分配到当前负载最低的核心(如
fork()后由调度器决定)。
3. 亲和性策略失效
- 未绑定CPU亲和性:进程未通过
sched_setaffinity绑定核心时,调度器可自由迁移。 - 动态调整:即使绑定部分核心,若目标核心不可用(如故障),进程会被迁移至其他允许的核心。
三、跨核心调度的实现流程
1. 调度器决策阶段
- 数据采集:各核心通过
/proc/stat上报负载(如用户态时间、系统态时间、空闲时间)。 - 负载评估:计算各核心的负载均衡指标(如
load_balance_ratio),选择最优目标核心。
2. 进程迁移阶段
- 队列操作:
- 从源核心的运行队列中移除进程。
- 将进程加入目标核心的运行队列。
- 上下文保存:仅保存线程私有上下文(寄存器、栈指针),无需切换页表。
3. 恢复执行阶段
- 抢占检查:目标核心若空闲,立即执行迁移后的进程;否则加入就绪队列等待调度。
四、实际案例分析
案例1:I/O密集型进程迁移
- 场景:进程A在核心0执行磁盘I/O阻塞,核心1空闲。
- 调度流程:
- I/O完成时,进程A被标记为就绪。
- 调度器检测到核心1负载低,将A迁移至核心1执行。
- 性能收益:减少核心0的I/O等待空闲时间,提升整体吞吐量。
案例2:CPU密集型进程均衡
- 场景:核心0运行高负载计算任务(如视频编码),核心1空闲。
- 调度流程:
- 调度器周期性检查负载,发现核心0利用率>90%。
- 将部分计算任务(如帧解码)迁移至核心1并行处理。
- 性能收益:缩短任务总执行时间(并行加速)。
五、限制与优化策略
1. 迁移开销限制
- 缓存失效:进程迁移导致L1/L2缓存失效,需权衡迁移收益与开销。
- 阈值设置:Linux内核默认仅在负载差异>20%时触发迁移。
2. 优化策略
- NUMA感知调度:优先将进程分配到本地内存节点所在的核心,减少跨节点访问延迟。
- 任务窃取(Work Stealing):空闲核心从繁忙核心的任务队列尾部窃取任务(如Go调度器)。
六、总结
多核CPU中进程的跨核心调度是动态负载均衡的核心机制,通过以下方式实现资源优化:
- 实时监控负载:基于CFS等算法评估各核心压力。
- 灵活迁移策略:根据进程状态(阻塞/就绪)和核心负载动态调整。
- 性能与开销平衡:通过NUMA感知、任务窃取等策略减少迁移代价。
应用建议:
- 对实时性要求高的任务,建议绑定CPU亲和性以避免迁移干扰。
- 对计算密集型任务,可通过
taskset手动分配到高性能核心(如Intel E5的高频核心)。
4、进程与线程的底层内存模型详解
进程和线程的底层完整内存模型分别是什么?
一、进程的完整内存模型
进程是操作系统资源分配的基本单位,其内存模型基于虚拟地址空间实现,包含以下核心区域(以32位Linux为例):
1. 内存布局
低地址 → 高地址
+-------------------+ 0x00000000
| 代码段 (Text) | 只读,存放编译后的机器指令
+-------------------+
| 数据段 (Data) | 已初始化的全局/静态变量
+-------------------+
| BSS段 | 未初始化的全局/静态变量(不占物理空间)
+-------------------+
| 堆 (Heap) | 动态分配内存(malloc/new),向上增长
+-------------------+
| 共享库映射区 | 动态链接库(如libc.so)的映射
+-------------------+
| 栈 (Stack) | 函数调用栈、局部变量,向下增长
+-------------------+
| 内核空间 | 操作系统内核代码和数据(不可直接访问)
+-------------------+ 0xFFFFFFFF
2. 关键特性
- 虚拟地址空间隔离:每个进程拥有独立的4GB虚拟地址空间(32位系统),通过页表映射到物理内存,实现内存隔离。
- 共享内核空间:所有进程共享内核空间(如系统调用接口、进程调度数据),但用户态无法直接访问。
- 内存分配策略:
- 堆:通过
brk/sbrk系统调用扩展,支持动态分配(如C的malloc)。 - 栈:由操作系统自动管理,函数调用时分配栈帧,返回时释放。
- 堆:通过
3. 进程间内存隔离机制
- 页表权限控制:每个进程的页表标记内存页为只读/可读写/不可执行,防止非法访问。
- 写时复制(COW):
fork()创建子进程时共享父进程内存,仅在修改时复制物理页。
二、线程的完整内存模型
线程是进程内的执行单元,共享进程的虚拟地址空间,但拥有独立的线程上下文。
1. 内存布局(以Linux线程为例)
进程地址空间(共享)
+-------------------+
| 代码段 | 所有线程共享
+-------------------+
| 数据段 | 全局变量、静态变量
+-------------------+
| 堆 | 动态分配内存(如C++的new)
+-------------------+
| 线程栈(私有) | 每个线程独立,存放局部变量、函数调用链
+-------------------+
| 寄存器状态 | 程序计数器、栈指针、通用寄存器
+-------------------+
2. 关键特性
- 共享内存:线程共享进程的代码段、数据段、堆和全局变量,可直接访问其他线程的堆数据(需同步机制)。
- 独立栈空间:
- 栈大小:默认8MB(Linux),通过
ulimit -s调整。 - 栈帧结构:每个函数调用分配栈帧,包含局部变量、返回地址、参数传递区。
- 栈大小:默认8MB(Linux),通过
- 寄存器上下文:线程切换时需保存/恢复寄存器状态(如EIP、ESP),但无需切换页表。
3. 线程间数据交互
- 共享数据:通过堆或全局变量直接读写(需加锁避免竞态条件)。
- 私有数据:局部变量、函数参数存储在线程栈中,其他线程无法直接访问。
三、进程与线程内存模型的核心差异
| 维度 | 进程 | 线程 |
|---|---|---|
| 地址空间 | 独立虚拟地址空间 | 共享进程地址空间 |
| 内存开销 | 高(需复制页表、堆等) | 低(仅分配栈和寄存器上下文) |
| 切换开销 | 高(涉及TLB刷新、页表切换) | 低(仅保存/恢复寄存器和栈指针) |
| 数据隔离 | 完全隔离(需IPC通信) | 共享数据(需同步机制) |
| 创建方式 | fork()或CreateProcess() |
pthread_create()或std::thread |
四、内存模型底层实现机制
1. 进程内存管理
- 页表机制:每个进程维护独立的页表(如x86的CR3寄存器),将虚拟地址映射到物理页框。
- 内存分配器:
- 用户态:
malloc/new通过brk/mmap向内核申请内存。 - 内核态:
kmalloc/vmalloc管理内核对象内存。
- 用户态:
2. 线程栈管理
- 栈增长策略:向下增长,当栈指针(ESP)低于栈底时触发栈溢出(Segmentation Fault)。
- 栈保护机制:栈溢出检测(如Canary值)、非执行栈(NX Bit)。
3. 线程上下文切换
- 保存内容:程序计数器(EIP/RIP)、栈指针(ESP/RSP)、通用寄存器(EAX/ECX等)。
- 恢复流程:从线程控制块(TCB)加载上下文,跳转到程序计数器继续执行。
五、实际案例分析
案例1:多线程共享堆数据
// 线程A修改堆数据
void* thread_func(void* arg) {
int* shared_data = (int*)arg;
*shared_data = 42; // 直接修改堆内存
return NULL;
}
// 主线程创建线程并传递堆指针
int main() {
int* data = malloc(sizeof(int));
pthread_t t;
pthread_create(&t, NULL, thread_func, data);
pthread_join(t, NULL);
printf("%d\n", *data); // 输出42
free(data);
return 0;
}
- 内存模型:
data指针指向堆内存,线程A和主线程共享该内存区域,需通过互斥锁保护。
案例2:线程栈独立性
void thread_func() {
char buffer[1024]; // 分配在线程栈上
strcpy(buffer, "Hello from thread");
// 其他线程无法直接访问buffer
}
- 内存模型:
buffer位于线程私有栈中,生命周期与线程绑定,栈帧释放后内存自动回收。
六、总结
- 进程内存模型:通过虚拟地址空间实现资源隔离,适合需要独立内存管理的任务(如Web服务器进程)。
- 线程内存模型:共享地址空间提升通信效率,但需处理同步问题,适合高并发计算(如并行计算框架)。
设计建议:
- 对内存敏感的任务(如数据库服务),优先使用进程隔离。
- 对计算密集型任务(如图像处理),使用线程充分利用多核。
本文来自博客园,作者:哈罗·沃德,转载请注明原文链接:https://www.cnblogs.com/panhua/p/19210434
浙公网安备 33010602011771号