Linux 进程调度模块

1. 进程与线程的本质

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

1.1 底层数据结构

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

cpp 复制代码
// 简化版 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(),内核态经历复杂的资源复制与初始化。

cpp 复制代码
用户态: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 调用链

cpp 复制代码
用户态: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_NORMAL CFS 完全公平调度:红黑树 + 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() 点,长循环中主动检查是否需要调度。
相关推荐
17(无规则自律)2 小时前
【Linux驱动实战】:最简单的内核模块
linux·c语言·驱动开发·嵌入式硬件
BioRunYiXue2 小时前
甘油不够了,能用植物油保存菌种吗?
java·linux·运维·服务器·网络·人工智能·eclipse
羸弱的穷酸书生2 小时前
跟AI学一手之运维Agent
运维·人工智能·agent
zhougl9962 小时前
Maven build配置
java·linux·maven
Predestination王瀞潞2 小时前
CentOS7虚拟机安装过程中没有打开网卡,ip addr无法查看es33这个情况下的解决方法
服务器·网络·tcp/ip
jianghao20253 小时前
realesrgan-gui跨平台使用指南:Win/Mac/Linux全支持
linux·windows·mac·跨平台软件·realesrgan-gui
Stark-C3 小时前
专为NAS用户打造的导航页,支持Docker管理,极空间部署FlatNas
运维·docker·容器
小王要努力上岸3 小时前
运维自动化工具 Ansible
运维·自动化·ansible
小码吃趴菜3 小时前
服务器预约系统linux小项目-第二节课
linux·运维·服务器