找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1709

积分

1

好友

242

主题
发表于 3 天前 | 查看: 7| 回复: 0

引言:从“尽快完成”到“必须在截止时间前完成”

想象你正在指挥一场交响乐团。乐团里有两种演奏者:

普通演奏者(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,改善明显!



上一篇:尚融宝400多集课程 P2P金融信贷平台全栈项目实战 SpringCloud+Vue+MyBatis-Plus一站式深度解析
下一篇:2023年最新JAVA架构师教程 100集JAVA架构师最尖锐的技术剖析 Spring/微服务/JVM/Redis/MySQL/Netty源码与高并发实战
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-24 19:13 , Processed in 0.188851 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表