当前位置: 首页 > news >正文

Linux 进程调度模块

1. 进程与线程的本质

在 Linux 内核中,进程和线程没有本质区别,它们统一被称为任务(Task)

1.1 底层数据结构

每个任务在内核中都由一个struct task_struct结构体描述,位于内核空间。它是进程/线程的身份证。

// 简化版 task_struct 关键字段 struct task_struct { volatile long state; // 任务状态 (RUNNING, SLEEPING, etc.) void *stack; // 内核栈指针 atomic_t usage; // 引用计数 unsigned int flags; // 标志位 pid_t pid; // 线程 ID (TID) pid_t tgid; // 进程组 ID (TGID),即通常理解的 PID struct mm_struct *mm; // 内存描述符 (页表、堆栈等) struct files_struct *files; // 打开的文件表 struct fs_struct *fs; // 文件系统信息 (当前目录等) struct sched_entity se; // 调度实体 (用于 CFS 调度) // ... 更多字段 };
  • 进程:拥有独立的 mm_struct(地址空间),pid 与 tgid 相同。
  • 线程:共享同一 mm_struct、files、fs,但拥有独立的 stack 和 pid,且 pid != tgid。

1.2 进程创建流程

1.2.1 调用链

用户态调用 fork(),内核态经历复杂的资源复制与初始化。

用户态:fork() ↓ (glibc 封装) 系统调用:sys_fork() / sys_clone() ↓ (内核入口) kernel/fork.c: _do_fork() (5.x 内核中由 kernel_clone 封装) ↓ kernel/fork.c: copy_process() ↓ copy_files(), copy_fs(), copy_mm(), copy_thread() ... ↓ kernel/fork.c: wake_up_new_task() ↓ 调度器:将新任务加入运行队列

1.2.2 核心步骤

1. 分配 task_struct (dup_task_struct)

内核为新任务分配内存(通常通过 kmem_cache_alloc 从 task_struct 缓存池获取),并复制父任务的 task_struct 内容。此时子任务几乎和父任务一模一样。

2. 资源复制与共享 (copy_* 系列函数)

这是区分进程和线程的关键点。copy_process 会调用一系列函数处理不同资源:

  • copy_files(): 复制文件描述符表。默认子进程继承父进程打开的文件(文件偏移量共享,但 fd 表独立)。
  • copy_fs(): 复制文件系统信息(如当前工作目录)。
  • copy_mm():最关键的一步
    • 进程 (fork): 复制 mm_struct 结构体,但不复制物理内存
    • 线程 (clone): 直接共享父任务的 mm_struct 指针。
  • copy_thread(): 设置子任务的内核栈和寄存器上下文(如指令指针 IP 设置为 fork 返回点)。

3. 写时复制 (Copy-On-Write, COW)

fork 创建进程之所以快,核心在于COW 机制

  • 原理:在 copy_mm 中,内核将父子进程的页表项(PTE)都设置为只读
  • 触发:当任意一方尝试内存时,CPU 触发页错误异常。
  • 处理:内核捕获异常,分配新的物理页,复制数据,修改页表指向新页,并恢复写权限。
  • 收益:如果 fork 后立即 exec,则无需复制任何内存,极大提升效率。
  • 即:fork进程时,不分配物理页,此时拥有的是父进程的只读页,写的时候触发错误才分配物理页并复制数据。

4. PID 分配 (alloc_pid)

内核通过 PID 命名空间分配唯一的 pid 和 tgid。

5. 加入运行队列 (wake_up_new_task)

新任务状态置为 TASK_RUNNING,并调用调度器接口将其插入到 CPU 的Runqueue中。此时它已具备被调度的资格。

1.3 线程创建流程

1.3.1 调用链

用户态:pthread_create() ↓ (glibc/NPTL) 系统调用:clone() ↓ 内核:_do_fork(clone_flags, ...)

1.3.2 区别

pthread_create 调用 clone 时,会传入一组特定的标志位,告诉内核“哪些资源要共享”:

标志位

含义

进程 (fork)

线程 (pthread)

CLONE_VM

共享内存空间 (mm_struct)

❌ (复制)

✅ (共享)

CLONE_FS

共享文件系统信息

❌ (复制)

✅ (共享)

CLONE_FILES

共享文件描述符表

❌ (复制)

✅ (共享)

CLONE_THREAD

放入同一线程组 (tgid 相同)

CLONE_SIGHAND

共享信号处理函数

❌ (复制)

✅ (共享)

特有的底层处理:

  1. 独立栈:虽然共享 mm_struct,但线程必须有独立的栈空间(用户栈和内核栈)。copy_thread 中会指定新的栈指针。
  2. TLS (Thread Local Storage):内核协助设置线程局部存储,确保 errno 等变量线程隔离。
  3. 调度实体:每个线程都有独立的 sched_entity,意味着线程是独立调度的。操作系统调度的是线程,而非进程。

2. 进程调度

Linux 默认使用CFS (Completely Fair Scheduler,完全公平调度器)调度 SCHED_NORMAL 任务。

2.1 调度器类

内核支持多种调度策略,按优先级从高到低:

  1. SCHED_DEADLINE: 基于最早截止时间优先 (EDF),用于硬实时。
  2. SCHED_FIFO / SCHED_RR: 实时调度类,优先级固定,不计算权重。
  3. SCHED_NORMAL (CFS): 普通进程,基于动态优先级和虚拟时间。
  4. SCHED_IDLE: 优先级最低,系统空闲时运行。

策略

调度算法

适用场景

SCHED_FIFO

先进先出(非抢占式时间片)

硬实时任务。一旦运行,除非主动阻塞或让出,否则一直占用 CPU,直到被更高优先级的实时任务抢占。

SCHED_RR

时间片轮转(Round Robin)

需要公平共享 CPU 的实时任务。同优先级的任务轮流执行,用完时间片后自动放到队列尾部。

SCHED_NORMALCFS 完全公平调度:红黑树 + vruntime(虚拟运行时间)普通交互式/后台任务(默认策略)。无固定时间片,追求长期公平。

SCHED_DEADLINE

EDF(Earliest Deadline First)

最复杂的实时调度。基于截止时间 (didi​)、运行时间 (cici​) 和周期 (

2.2 CFS原理

2.2.1 管理结构

每个 CPU 都有一个运行队列 (struct rq)。CFS 使用红黑树来管理可运行任务。

  • 节点:struct sched_entity (嵌入在 task_struct 中)。
  • 排序键值:vruntime (Virtual Runtime,虚拟运行时间,优先级越高vruntime越小)。
  • 规则:vruntime 越小的任务,在红黑树越左侧,越优先被调度。

2.2.2 调度流程

当发生调度时(如时间片用完、进程阻塞、更高优先级任务唤醒),内核调用 schedule():

  1. 选择下一个任务:获取红黑树的最左节点,即 vruntime 最小的任务。
  2. 上下文切换
    • 保存现场:将当前寄存器(RIP, RSP, RBX 等)保存到 prev->thread 结构体。
    • 切换栈:切换内核栈指针 (TSS 寄存器或 RSP)。
    • 切换地址空间:如果 prev->mm != next->mm,切换页表全局目录寄存器 (CR3),刷新TLB
    • 恢复现场:从 next->thread 恢复寄存器,跳转到 next 上次执行的指令继续运行。

2.2.3 调度触发时机

  1. 主动调度:进程调用 sleep(), wait(), read() (无数据) 等阻塞接口,状态变为 TASK_INTERRUPTIBLE,主动让出 CPU。
  2. 被动调度
    • 时间片耗尽:硬件定时器中断触发,更新 vruntime,若当前任务 vruntime 不是最小,则触发重调度。
    • 唤醒抢占:高优先级任务从阻塞中唤醒,若其 vruntime 远小于当前运行任务,触发 check_preempt_curr,设置 TIF_NEED_RESCHED 标志。
    • 返回用户态检查:在从中断或系统调用返回用户态前,检查 TIF_NEED_RESCHED,若置位则调用 schedule()。

2.2.4 多核CPU

现代 CPU 是多核的。为了减少锁竞争,每个逻辑 CPU 都有独立的 struct rq 和 红黑树。

  • 优点:大部分调度操作无需加锁,并发性能高。
  • 缺点:可能导致负载不均(一个核忙死,一个核空闲)。

内核定期运行负载均衡器:

  1. 检测:发现某些 CPU 的 rq 任务过多,某些过少。
  2. 迁移:将任务从一个 CPU 的 rq 移动到另一个 CPU 的 rq。
  3. 缓存亲和性:迁移时会尽量考虑 L1/L2/L3 缓存共享关系(如优先在同一物理核心的超线程间迁移,或同一 NUMA 节点内迁移),以减少缓存失效带来的性能损耗。

抢占:

  • 内核抢占:允许在内核态执行期间被更高优先级的任务打断。
  • 自愿抢占:内核中有一些显式的 cond_resched() 点,长循环中主动检查是否需要调度。
http://www.jsqmd.com/news/500079/

相关文章:

  • 第5篇:中文语言 华夏本源语言——实战代码示例
  • 从全红90%到安全线!这篇降AI保姆级实操攻略,看完不再慌(附工具避坑测评)
  • 深度解析:OpenClaw 企业级安全加固架构设计与实现 - 从权限泛滥到零信任的完整实践
  • 被问爆的下载工具,跑满200M带宽,下载速度25.9MB/s
  • uc/os-II操作系统时钟节拍器
  • 什么是偶极矩?
  • LangBot:企业级即时通讯 AI 机器人平台 使用包管理器部署(包含手机部署方式)篇
  • 在 Windows 中打开蓝牙设备
  • AI大模型竟被投毒?315曝光的“暗黑操作”如何影响你的决策?
  • 计算机毕业设计答辩全攻略,答辩老师最常问的都在这里了
  • 二叉树详解:从概念到应用,带你玩转树形结构
  • 【卡尔曼滤波】第1章 理论基础与标准卡尔曼滤波
  • 一、Spring
  • 号码核验在B端拓客中的应用困境与技术升级路径研究氪迹科技法人号码核验系统
  • 软件开发常用工具介绍
  • HTTP 消息:解析与优化
  • LoRA 与 QLoRA
  • Zabbix6.2利用模板和自定义监控项监控华为AR3260路由器
  • ROS2学习记录009-使用面向对象方式编写ROS2节点
  • 从此告别拖延!全场景通用的AI论文工具 —— 千笔写作工具
  • 震惊,杨幂的脸竟然出现在了她的身体上
  • java基础学习3(数据类型转换、运算符)
  • 把坑都踩完了,千笔AI VS 笔捷Ai,全场景通用AI论文网站!
  • 【常见错误】Xilinx Vivado自带编辑器文字部分出现乱码解决办法
  • 数字孪生国内外发展现状
  • 【Xilinx Vivado时序分析/约束系列2】FPGA开发时序分析/约束-建立时间
  • 终极指南:使用Google Map React库快速构建交互式地图应用
  • JetBrains 插件 IDE设置
  • 学霸同款!全领域适配的论文神器 —— 千笔
  • STM32-串口使用注意事项