操作系统进程调度算法实战:链表数据结构在FCFS/SJF/RR中的5种应用
操作系统进程调度算法实战:链表数据结构在FCFS/SJF/RR中的5种应用
1. 进程调度与链表数据结构的天然契合
当多个进程在操作系统中并发执行时,如何高效管理就绪队列成为系统性能的关键。链表数据结构以其动态内存管理和灵活插入删除的特性,成为实现进程控制块(PCB)队列的理想选择。不同于数组的固定大小限制,链表可以随着进程的创建和终止动态伸缩,完美适配操作系统运行时的不确定性需求。
单向链表和队列在调度算法中展现出不同的优势:
- 单向链表:适用于需要频繁遍历和随机插入的场景,如SJF算法中的有序插入
- 队列:严格遵循FIFO原则,是FCFS和RR算法的自然选择
在Linux内核的进程调度器CFS中,就使用了红黑树这种自平衡二叉搜索树来管理可运行进程,但其本质仍是对链表特性的扩展。我们来看一个典型的PCB链表节点定义:
typedef struct _Pcb { int pid; // 进程ID int arrive_time; // 到达时间 int burst_time; // 执行时间 int remaining_time; // 剩余时间 int priority; // 优先级 struct _Pcb *next; // 下一个PCB指针 } PCB;这个结构体封装了进程调度的核心参数,其中next指针构成了链表的基础。通过不同的链表操作组合,我们可以实现三大经典调度算法:
| 算法特性 | FCFS | SJF | RR |
|---|---|---|---|
| 链表类型 | 队列 | 有序链表 | 循环队列 |
| 插入位置 | 队尾 | 按执行时间 | 队尾 |
| 删除位置 | 队首 | 队首 | 队首 |
| 时间复杂度 | O(1) | O(n) | O(1) |
2. 先来先服务(FCFS)的队列实现
FCFS算法如同银行的排队叫号系统,完美诠释了"先到先得"的公平原则。其链表实现需要维护一个简单的队列结构:
PCB* enqueue(PCB* tail, PCB* new_process) { if (tail == NULL) { new_process->next = new_process; // 循环队列自引用 return new_process; } new_process->next = tail->next; tail->next = new_process; return new_process; } PCB* dequeue(PCB* tail) { if (tail == NULL) return NULL; if (tail->next == tail) { free(tail); return NULL; } PCB* head = tail->next; tail->next = head->next; free(head); return tail; }FCFS的调度过程呈现出明显的顺序特性:
- 新进程到达时,调用
enqueue插入队尾 - 当前进程完成时,调用
dequeue移除队首进程 - 从队首取出下一个进程执行
我们通过一个具体案例观察其表现:
| 进程 | 到达时间 | 执行时间 | 开始时间 | 完成时间 | 周转时间 |
|---|---|---|---|---|---|
| P1 | 0 | 5 | 0 | 5 | 5 |
| P2 | 1 | 3 | 5 | 8 | 7 |
| P3 | 2 | 8 | 8 | 16 | 14 |
注意:FCFS算法虽然实现简单,但可能存在"护航效应"(Convoy Effect),即长进程后的短进程需要等待过长时间。上表中P2虽然执行时间短,却因P1的执行而等待了4个时间单位
3. 短作业优先(SJF)的有序链表管理
SJF算法通过预测进程执行时间优化系统吞吐量,其链表实现需要维护一个按执行时间排序的有序链表:
PCB* insert_sjf(PCB* head, PCB* new_process) { if (head == NULL || new_process->burst_time < head->burst_time) { new_process->next = head; return new_process; } PCB* current = head; while (current->next != NULL && current->next->burst_time < new_process->burst_time) { current = current->next; } new_process->next = current->next; current->next = new_process; return head; }SJF算法的实际调度需要考虑进程到达时间的动态变化。以下是改进后的调度逻辑:
void schedule_sjf(PCB* head) { int current_time = 0; while (head != NULL) { PCB* shortest = extract_shortest(&head, current_time); if (shortest == NULL) { current_time++; // CPU空闲 continue; } printf("执行进程%d: %d-%d\n", shortest->pid, current_time, current_time + shortest->burst_time); current_time += shortest->burst_time; free(shortest); } }SJF算法在平均等待时间指标上表现优异:
| 算法 | 平均等待时间 | 平均周转时间 |
|---|---|---|
| FCFS | 5.33 | 8.67 |
| SJF | 3.33 | 6.67 |
关键点:在实际系统中,精确预测进程执行时间非常困难。Linux的CFS调度器通过虚拟运行时间(vruntime)来近似实现类似效果,计算公式为:vruntime += delta_exec * (NICE_0_LOAD / weight)
4. 时间片轮转(RR)的循环队列技巧
RR算法通过引入时间量子(Time Quantum)实现公平调度,其循环队列实现需要特别关注:
typedef struct { PCB *front, *rear; } RRQueue; void rr_enqueue(RRQueue *q, PCB *process) { process->next = NULL; if (q->rear == NULL) { q->front = q->rear = process; return; } q->rear->next = process; q->rear = process; } PCB* rr_dequeue(RRQueue *q) { if (q->front == NULL) return NULL; PCB *temp = q->front; q->front = q->front->next; if (q->front == NULL) q->rear = NULL; return temp; }RR算法的核心调度逻辑需要考虑时间片中断和进程状态保存:
void round_robin(RRQueue *q, int quantum) { int current_time = 0; while (q->front != NULL) { PCB *proc = rr_dequeue(q); int execute_time = min(quantum, proc->remaining_time); printf("时间%d-%d: 执行进程%d (%d/%d)\n", current_time, current_time + execute_time, proc->pid, execute_time, proc->burst_time); proc->remaining_time -= execute_time; current_time += execute_time; if (proc->remaining_time > 0) { rr_enqueue(q, proc); // 重新入队 } else { printf("进程%d完成! 总周转时间: %d\n", proc->pid, current_time - proc->arrive_time); free(proc); } } }时间片大小对系统性能有显著影响:
| 时间片 | 上下文切换次数 | 平均响应时间 | 吞吐量 |
|---|---|---|---|
| 1 | 高 | 优 | 低 |
| 5 | 中 | 良 | 中 |
| 20 | 低 | 一般 | 高 |
现代操作系统通常采用10-100ms的时间片,在响应速度和系统开销之间取得平衡。Linux的CFS调度器更进一步,采用动态时间片策略,根据系统负载和进程优先级自动调整。
5. 三种算法的链表操作对比与性能分析
通过对比三种算法的链表操作差异,我们可以深入理解其性能特点:
FCFS队列操作:
- 入队:O(1) 直接追加到队尾
- 出队:O(1) 从队首移除
- 适用场景:批处理系统,进程执行时间相近
SJF有序链表操作:
- 插入:O(n) 需要找到合适位置
- 删除:O(1) 总是移除首元素
- 优化:可使用优先队列将插入优化到O(log n)
RR循环队列操作:
- 入队:O(1) 追加到队尾
- 出队:O(1) 从队首移除
- 特殊处理:完成进程需要释放内存
我们通过一个综合案例比较三种算法的表现:
假设有如下进程序列到达:
- P1: 到达时间0,执行时间5
- P2: 到达时间1,执行时间3
- P3: 到达时间2,执行时间8
- P4: 到达时间3,执行时间6
各算法的性能指标对比:
| 指标 | FCFS | SJF | RR(量子=4) |
|---|---|---|---|
| 平均等待时间 | 7.0 | 5.25 | 6.5 |
| 平均周转时间 | 11.0 | 9.25 | 10.5 |
| 最大等待时间 | 14 | 9 | 12 |
从实现角度看,三种算法在链表管理上有共同点也有差异:
// 共同的基础操作 PCB* create_pcb(int pid, int arrive, int burst) { PCB* new = (PCB*)malloc(sizeof(PCB)); new->pid = pid; new->arrive_time = arrive; new->burst_time = burst; new->remaining_time = burst; new->next = NULL; return new; } // 不同的调度策略 void schedule(PCB* head, AlgorithmType type) { switch(type) { case FCFS: /* 队列管理 */ break; case SJF: /* 有序链表 */ break; case RR: /* 循环队列 */ break; } }在实际系统设计中,往往会采用混合策略。如Linux的CFS调度器就结合了红黑树和时间片轮转的思想,既保证公平性又兼顾吞吐量。Windows NT内核则采用多级反馈队列,动态调整进程优先级和时间片大小。