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 | 共享信号处理函数 | ❌ (复制) | ✅ (共享) |
特有的底层处理:
- 独立栈:虽然共享 mm_struct,但线程必须有独立的栈空间(用户栈和内核栈)。copy_thread 中会指定新的栈指针。
- TLS (Thread Local Storage):内核协助设置线程局部存储,确保 errno 等变量线程隔离。
- 调度实体 :每个线程都有独立的 sched_entity,意味着线程是独立调度的。操作系统调度的是线程,而非进程。
2. 进程调度
Linux 默认使用 CFS (Completely Fair Scheduler,完全公平调度器) 调度 SCHED_NORMAL 任务。
2.1 调度器类
内核支持多种调度策略,按优先级从高到低:
- SCHED_DEADLINE: 基于最早截止时间优先 (EDF),用于硬实时。
- SCHED_FIFO / SCHED_RR: 实时调度类,优先级固定,不计算权重。
- SCHED_NORMAL (CFS): 普通进程,基于动态优先级和虚拟时间。
- 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():
- 选择下一个任务 :获取红黑树的 最左节点,即 vruntime 最小的任务。
- 上下文切换 :
- 保存现场:将当前寄存器(RIP, RSP, RBX 等)保存到 prev->thread 结构体。
- 切换栈:切换内核栈指针 (TSS 寄存器或 RSP)。
- 切换地址空间 :如果 prev->mm != next->mm,切换页表全局目录寄存器 (CR3),刷新 TLB。
- 恢复现场:从 next->thread 恢复寄存器,跳转到 next 上次执行的指令继续运行。
2.2.3 调度触发时机
- 主动调度:进程调用 sleep(), wait(), read() (无数据) 等阻塞接口,状态变为 TASK_INTERRUPTIBLE,主动让出 CPU。
- 被动调度 :
- 时间片耗尽:硬件定时器中断触发,更新 vruntime,若当前任务 vruntime 不是最小,则触发重调度。
- 唤醒抢占:高优先级任务从阻塞中唤醒,若其 vruntime 远小于当前运行任务,触发 check_preempt_curr,设置 TIF_NEED_RESCHED 标志。
- 返回用户态检查:在从中断或系统调用返回用户态前,检查 TIF_NEED_RESCHED,若置位则调用 schedule()。
2.2.4 多核CPU
现代 CPU 是多核的。为了减少锁竞争,每个逻辑 CPU 都有独立的 struct rq 和 红黑树。
- 优点:大部分调度操作无需加锁,并发性能高。
- 缺点:可能导致负载不均(一个核忙死,一个核空闲)。
内核定期运行负载均衡器:
- 检测:发现某些 CPU 的 rq 任务过多,某些过少。
- 迁移:将任务从一个 CPU 的 rq 移动到另一个 CPU 的 rq。
- 缓存亲和性:迁移时会尽量考虑 L1/L2/L3 缓存共享关系(如优先在同一物理核心的超线程间迁移,或同一 NUMA 节点内迁移),以减少缓存失效带来的性能损耗。
抢占:
- 内核抢占:允许在内核态执行期间被更高优先级的任务打断。
- 自愿抢占:内核中有一些显式的 cond_resched() 点,长循环中主动检查是否需要调度。