引言:从“尽快完成”到“必须在截止时间前完成”
想象你正在指挥一场交响乐团。乐团里有两种演奏者:
普通演奏者(CFS进程):
- 按乐谱大致的时间演奏
- 偶尔快一点慢一点观众不会注意
- 追求整体和谐而非绝对精确
打击乐手(实时进程):
- 必须在第32小节的第3拍准时敲响铜锣
- 早1毫秒或晚1毫秒都会破坏整个乐曲
- 要求绝对精确的时间控制
在计算世界中,实时进程就是那些必须在一定时间内完成响应的任务。在工业控制、自动驾驶或音视频处理等场景中,任务的时效性至关重要。
第一部分:实时性的严格定义
1.1 实时系统的分类
软实时(Soft Real-Time):
- 错过截止时间不会导致灾难性后果
- 性能会下降,但系统仍能工作
- 示例:视频播放器(掉帧可以接受)
硬实时(Hard Real-Time):
- 错过截止时间会导致系统失败或灾难
- 必须在最坏情况下也保证响应时间
- 示例:飞机控制系统、医疗设备
Linux的定位:标准Linux内核是软实时系统,但通过PREEMPT_RT补丁可以接近硬实时性能。
1.2 Linux实时调度策略的三驾马车
Linux提供了三种实时调度策略,每种都有不同的适用场景:
// 调度策略定义(include/uapi/linux/sched.h)
#define SCHED_NORMAL 0 // 普通进程(CFS)
#define SCHED_FIFO 1 // 先进先出实时策略
#define SCHED_RR 2 // 轮转实时策略
#define SCHED_BATCH 3 // 批处理策略
#define SCHED_IDLE 5 // 空闲策略
#define SCHED_DEADLINE 6 // 截止时间调度(Linux 3.14+)
运维视角:
- SCHED_FIFO:优先级最高,不主动让出就一直运行
- SCHED_RR:同优先级进程轮流运行,有固定时间片
- SCHED_DEADLINE:按截止时间调度,理论最优的实时策略
1.3 实时优先级体系
实时进程的优先级范围是1-99(数值越大优先级越高),而普通进程的nice值映射到100-139:
# 查看进程的优先级信息
ps -eo pid,comm,rtprio,ni,pri,policy | head -10
# 输出示例:
# PID COMMAND RTPRIO NI PRI POL
# 1 systemd - 0 19 TS # 普通进程,TS=SCHED_OTHER
# 123 rt-app 99 0 139 FF # 实时进程,FF=SCHED_FIFO,优先级99
# 124 rt-task 80 0 120 RR # 实时进程,RR=SCHED_RR,优先级80
# 字段解释:
# RTPRIO: 实时优先级(-表示非实时进程)
# NI: nice值(-20到19)
# PRI: 内核优先级(0-139,数值越小优先级越高)
# POL: 调度策略(TS=SCHED_OTHER, FF=SCHED_FIFO, RR=SCHED_RR)
关键规律:内核优先级数值越小优先级越高,实时进程(0-99)总是优先于普通进程(100-139)。
第二部分:实时调度算法深度解析
2.1 SCHED_FIFO:简单而强大
SCHED_FIFO(先进先出)是最简单的实时策略:
// SCHED_FIFO的核心逻辑(简化)
void schedule_fifo(struct rq *rq) {
// 1. 从最高优先级开始查找
for (int prio = MAX_RT_PRIO-1; prio >= 0; prio--) {
// 2. 如果该优先级有可运行进程
if (rt_rq->active.queue[prio]) {
// 3. 选择该队列的第一个进程
task = list_first_entry(&rt_rq->active.queue[prio],
struct task_struct, rt.run_list);
// 4. 一直运行直到:
// a) 自愿放弃CPU(阻塞、调用sched_yield)
// b) 被更高优先级进程抢占
// c) 进程结束
return task;
}
}
return NULL; // 没有实时进程
}
运维陷阱:一个SCHED_FIFO进程如果不主动让出CPU,会一直运行,可能导致系统无响应。
2.2 SCHED_RR:带时间片的实时调度
SCHED_RR在SCHED_FIFO基础上增加了时间片轮转:
// 默认时间片(kernel/sched/rt.c)
unsigned int sysctl_sched_rr_timeslice = 100 * 1000; // 100毫秒
// 更新实时进程时间片
static void update_curr_rt(struct rq *rq) {
struct task_struct *curr = rq->curr;
u64 delta_exec;
if (curr->sched_class != &rt_sched_class)
return;
// 计算从上一次调度以来的执行时间
delta_exec = rq_clock_task(rq) - curr->se.exec_start;
// 减少时间片
curr->rt.time_slice -= delta_exec;
// 时间片用完的处理
if (curr->rt.time_slice <= 0) {
// 重置时间片
curr->rt.time_slice = sched_rr_timeslice;
// 如果是SCHED_RR策略,将进程移到队列尾部
if (curr->policy == SCHED_RR) {
list_move_tail(&curr->rt.run_list,
curr->rt.run_list.prev);
// 设置需要重新调度标志
set_tsk_need_resched(curr);
}
}
}
运维价值:SCHED_RR适合优先级相同但有多个进程需要公平共享CPU的场景。
2.3 SCHED_DEADLINE:基于截止时间的最优调度
SCHED_DEADLINE是Linux 3.14引入的最强实时调度策略:
// 截止时间任务参数结构
struct sched_dl_entity {
u64 dl_runtime; // 最坏执行时间(Runtime)
u64 dl_deadline; // 相对截止时间(Deadline)
u64 dl_period; // 任务周期(Period)
u64 dl_bw; // 带宽 = dl_runtime / dl_period
u64 dl_density; // 密度 = dl_runtime / dl_deadline
// 动态计算字段
u64 dl_throttled; // 被限制的开始时间
s64 runtime; // 剩余运行时间
u64 deadline; // 绝对截止时间
};
// 调度算法:最早截止时间优先(EDF)
static struct task_struct *pick_next_task_dl(struct rq *rq) {
struct rb_node *left = rq->dl.rb_leftmost;
if (!left)
return NULL;
// 选择红黑树中截止时间最早的节点
return rb_entry(left, struct task_struct, dl.rb_node);
}
数学原理:SCHED_DEADLINE基于Liu和Layland的速率单调调度(RMS)和最早截止时间优先(EDF)理论。
可调度性测试:对于n个周期任务,系统可调度的条件是:
Σ(dl_runtime_i / dl_period_i) ≤ n(2^(1/n) - 1) // RMS
或
Σ(dl_runtime_i / dl_deadline_i) ≤ 1 // EDF
2.4 实时调度器的数据结构
实时调度器使用优先级位图快速查找最高优先级进程:
// 实时运行队列(kernel/sched/rt.c)
struct rt_rq {
struct rt_prio_array active; // 活动进程数组
unsigned int rt_nr_running; // 可运行实时进程数
unsigned int rt_throttled; // 是否被限制
u64 rt_time; // 已使用的实时时间
u64 rt_runtime; // 允许的实时时间
};
// 优先级数组
struct rt_prio_array {
DECLARE_BITMAP(bitmap, MAX_RT_PRIO+1); // 位图:1表示该优先级有进程
struct list_head queue[MAX_RT_PRIO]; // 每个优先级的链表
};
// 快速查找最高优先级的宏
#define sched_find_first_bit(map) __builtin_ctzl(*(map))
设计巧思:使用位图和内置函数__builtin_ctzl(计算末尾0的个数)实现O(1)时间复杂度的最高优先级查找。
第三部分:实时进程的配置与管理
3.1 设置实时调度策略
在编程中设置实时调度:
#include <sched.h>
#include <stdio.h>
#include <unistd.h>
int main() {
struct sched_param param;
pid_t pid = getpid();
// 设置SCHED_FIFO,优先级50
param.sched_priority = 50;
if (sched_setscheduler(pid, SCHED_FIFO, ¶m) == -1) {
perror("sched_setscheduler failed");
// 可能需要root权限或CAP_SYS_NICE能力
}
// 设置SCHED_DEADLINE(Linux 3.14+)
struct sched_attr attr = {
.size = sizeof(attr),
.sched_policy = SCHED_DEADLINE,
.sched_runtime = 10 * 1000 * 1000, // 10毫秒
.sched_deadline = 20 * 1000 * 1000, // 20毫秒
.sched_period = 20 * 1000 * 1000, // 20毫秒
};
if (sched_setattr(pid, &attr, 0) == -1) {
perror("sched_setattr failed");
}
return 0;
}
3.2 使用chrt命令管理实时进程
chrt是运维人员管理实时进程的主要工具:
# 1. 启动新进程为实时进程
chrt -f 50 ./real_time_app # SCHED_FIFO,优先级50
chrt -r 50 ./real_time_app # SCHED_RR,优先级50
# 2. 修改运行中进程的调度策略
chrt -f -p 50 1234 # 将PID 1234改为SCHED_FIFO,优先级50
# 3. 设置SCHED_DEADLINE(需要较新内核)
chrt -d --sched-runtime 10000000 \ # 10ms
--sched-deadline 20000000 \ # 20ms
--sched-period 20000000 \ # 20ms
0 ./deadline_task # 优先级为0(对DEADLINE无意义)
# 4. 查看进程的调度信息
chrt -p 1234
# 输出:pid 1234's current scheduling policy: SCHED_FIFO
# pid 1234's current scheduling priority: 50
3.3 系统范围的实时限制
为了防止实时进程饿死普通进程,Linux提供了全局限制:
# 查看系统实时限制
cat /proc/sys/kernel/sched_rt_period_us # 实时周期,默认1秒(1000000微秒)
cat /proc/sys/kernel/sched_rt_runtime_us # 实时运行时间,默认0.95秒(950000微秒)
# 这意味着:在1秒周期内,所有实时进程最多运行0.95秒
# 剩余的0.05秒留给普通进程
# 临时修改限制
echo 1000000 > /proc/sys/kernel/sched_rt_period_us
echo 800000 > /proc/sys/kernel/sched_rt_runtime_us # 限制为80%
# 永久修改(/etc/sysctl.conf)
kernel.sched_rt_period_us = 1000000
kernel.sched_rt_runtime_us = 800000
运维经验:在生产环境中,合理设置这些参数可以防止实时进程导致系统无响应。
第四部分:实时性能分析与优化
4.1 实时延迟测量工具
cyclictest:最常用的实时延迟测试工具
# 基本用法
cyclictest -t1 -p 80 -n -i 10000 -l 10000
# 参数解释:
# -t1: 1个测试线程
# -p 80: 优先级80
# -n: 使用clock_nanosleep
# -i 10000: 间隔10微秒
# -l 10000: 循环10000次
# 高级用法:多核测试
cyclictest -t5 -p 80 -n -i 1000 -q -a 0,1,2,3,4
# 输出解释:
# T: 线程号 P: 优先级 I: 间隔 C: 计数器
# Min: 最小延迟 Act: 最近延迟 Avg: 平均延迟 Max: 最大延迟
stress-ng:系统压力测试结合实时性测试
# 创建系统压力,同时测试实时延迟
stress-ng --cpu 4 --io 2 --vm 1 --vm-bytes 1G &
cyclictest -t1 -p 99 -n -i 1000 -l 10000 -q
4.2 ftrace实时调度跟踪
# 启用实时调度跟踪
echo 1 > /sys/kernel/debug/tracing/events/sched/enable
echo 1 > /sys/kernel/debug/tracing/events/sched/sched_switch/enable
echo 1 > /sys/kernel/debug/tracing/events/sched/sched_wakeup/enable
# 只跟踪特定进程
echo 'comm == "rt-task"' > /sys/kernel/debug/tracing/events/sched/sched_switch/filter
# 开始跟踪
echo 1 > /sys/kernel/debug/tracing/tracing_on
# 运行实时任务
./rt-task
# 分析结果
cat /sys/kernel/debug/tracing/trace | grep -A5 -B5 "rt-task"
4.3 实时延迟的常见原因与优化
要深入理解这些优化点,可以参考云栈社区的网络/系统板块中关于中断、并发和性能调优的更多讨论。
原因1:中断屏蔽时间过长
# 检查最大中断屏蔽时间
cat /sys/kernel/debug/tracing/trace_stat/function0 # 需要ftrace配置
# 优化:减少中断处理时间,使用线程化中断
echo threadirqs > /proc/cmdline # 内核启动参数
原因2:自旋锁争用
# 监控自旋锁争用
perf record -e lock:lock_acquire -a sleep 10
perf report
# 优化:使用读写锁、RCU等减少锁争用
原因3:内存管理延迟
# 监控页面错误
perf stat -e page-faults,major-faults ./rt-app
# 优化:预分配内存,mlock锁定内存
#include <sys/mman.h>
mlockall(MCL_CURRENT | MCL_FUTURE); // 锁定当前和未来分配的内存
原因4:缓存失效
# 检查缓存命中率
perf stat -e cache-references,cache-misses ./rt-app
# 优化:绑定进程到固定CPU,避免迁移
taskset -c 2 ./rt-app # 绑定到CPU2
第五部分:PREEMPT_RT补丁深度解析
5.1 PREEMPT_RT的四大改进
标准Linux内核不是硬实时的,PREEMPT_RT补丁通过以下改进接近硬实时:
1. 线程化中断(Threaded IRQs)
// 传统中断处理(不可抢占)
irqreturn_t irq_handler(int irq, void *dev_id) {
// 在中断上下文中运行,不可抢占
return IRQ_HANDLED;
}
// 线程化中断处理(可抢占)
irqreturn_t irq_handler(int irq, void *dev_id) {
// 快速部分:在硬中断中快速处理
return IRQ_WAKE_THREAD;
}
irqreturn_t irq_thread_fn(int irq, void *dev_id) {
// 慢速部分:在独立内核线程中运行,可被抢占
return IRQ_HANDLED;
}
2. 自旋锁转换为互斥锁(rtmutex)
// 标准自旋锁:忙等待,不可抢占
DEFINE_SPINLOCK(my_lock);
spin_lock(&my_lock);
// 临界区
spin_unlock(&my_lock);
// RT自旋锁:可睡眠的互斥锁
DEFINE_RT_MUTEX(my_lock);
rt_mutex_lock(&my_lock);
// 临界区,可被更高优先级任务抢占
rt_mutex_unlock(&my_lock);
3. 增加内核抢占点
// 标准内核:只有少量明确抢占点
might_sleep(); // 可能睡眠的提示
// RT内核:几乎处处可抢占
// 将许多内核操作标记为可抢占
4. 高精度定时器
// 标准定时器:基于jiffies(通常1ms或4ms精度)
mod_timer(&timer, jiffies + msecs_to_jiffies(10));
// 高精度定时器:纳秒级精度
hrtimer_start(&hrtimer, ktime_set(0, 10000000), HRTIMER_MODE_REL);
5.2 PREEMPT_RT内核配置
# 检查当前内核的抢占模式
uname -a
# 如果有"PREEMPT RT"字样表示实时补丁内核
# 查看抢占配置
zcat /proc/config.gz | grep -i preempt
# 关键配置选项:
# CONFIG_PREEMPT_NONE=y # 无抢占,适合服务器
# CONFIG_PREEMPT_VOLUNTARY=y # 自愿抢占,适合桌面
# CONFIG_PREEMPT=y # 完全抢占,适合桌面
# CONFIG_PREEMPT_RT=y # 实时抢占,打RT补丁后
# 编译实时内核的关键选项
CONFIG_PREEMPT_RT=y
CONFIG_HIGH_RES_TIMERS=y # 高精度定时器
CONFIG_NO_HZ_FULL=y # 全无滴答模式
CONFIG_THREAD_INFO_IN_TASK=y # 线程信息在task_struct中
CONFIG_PREEMPT_NOTIFIERS=y # 抢占通知
5.3 PREEMPT_RT性能对比
# 在标准内核和RT内核上分别测试
# 标准内核(CONFIG_PREEMPT_NONE)
cyclictest -t1 -p 99 -n -i 1000 -l 10000
# 结果:Min: 10μs, Avg: 22μs, Max: 850μs
# RT内核(CONFIG_PREEMPT_RT)
cyclictest -t1 -p 99 -n -i 1000 -l 10000
# 结果:Min: 8μs, Avg: 15μs, Max: 120μs
# 最大延迟从850μs降到120μs,改善明显!