串行流水线 + 消息并发,这是嵌入式ISP处理中最典型、也最棘手的场景。
好消息是:串行拓扑 让数学模型大大简化,不需要处理复杂的DAG分叉合并。但消息并发 引入了资源竞争,这才是建模的核心难点。
下面给出一套精确的确定性数学模型,专门针对"多消息在串行流水线上并发执行"的场景。
1. 问题形式化
1.1 系统参数(全部已知)
- CPU核心数 :KKK(固定)
- 单核算力 :α\alphaα(归一化因子,如 1.0)
- 流水线阶段 :PPP个线程节点(按顺序执行,固定映射到各核心)
- 硬件阶段:穿插在流水线中的硬件等待节点(固定时间)
- 每个阶段执行时间 :eie_iei(固定,已知)
- 消息到达时间 :t1,t2,t3,...t_1, t_2, t_3, ...t1,t2,t3,...(实际观测值,但建模时作为已知输入)
1.2 流水线拓扑(串行)
消息 M 的流水线:
[线程1 on Core A] → [硬件1 (10ms)] → [线程2 on Core B] → [硬件2 (5ms)] → [线程3 on Core C] → 完成
每个阶段只能在前一阶段完成后开始。
1.3 调度规则(固定策略)
- 每个核心维护一个FIFO队列(或优先级队列,但串行场景下通常同优先级FIFO)。
- 抢占?如果高优先级可抢占,稍复杂;先按非抢占式FIFO建模(最常用),后文扩展抢占式。
2. 核心数学模型:流水线调度 + 排队论
对于串行流水线,每个消息的完成时间可以递推计算 ,关键是要跟踪每个核心在每个时间点的队列状态。
2.1 符号定义
| 符号 | 含义 |
|---|---|
| MjM_jMj | 第 jjj个到达的消息 |
| tjt_jtj | 消息 MjM_jMj的到达时刻 |
| stagei\text{stage}_istagei | 流水线第 iii个阶段(i=1..Pi=1..Pi=1..P) |
| core(i)\text{core}(i)core(i) | 阶段 iii所在CPU核心编号 |
| eie_iei | 阶段 iii的执行时间(线程处理) |
| hih_ihi | 阶段 iii后的硬件等待时间(如果有) |
| Sj,iS_{j,i}Sj,i | 消息 MjM_jMj在阶段 iii的实际开始时间 |
| Fj,iF_{j,i}Fj,i | 消息 MjM_jMj在阶段 iii的实际完成时间 |
2.2 递推公式(核心)
阶段1(第一个线程):Sj,1=max(tj,核心 core(1) 在 tj 时刻的队列空闲时间)S_{j,1} = \max\left( t_j, \text{核心 } \text{core}(1) \text{ 在 } t_j \text{ 时刻的队列空闲时间} \right)Sj,1=max(tj,核心 core(1) 在 tj 时刻的队列空闲时间)Fj,1=Sj,1+e1F_{j,1} = S_{j,1} + e_1Fj,1=Sj,1+e1
阶段 iii(i≥2i \ge 2i≥2):
阶段 iii的理论就绪时间 (所有前置阶段完成):Rj,i=Fj,i−1+hi−1R_{j,i} = F_{j,i-1} + h_{i-1}Rj,i=Fj,i−1+hi−1
其中 hi−1h_{i-1}hi−1是阶段 i−1i-1i−1后的硬件等待时间(如果阶段 i−1i-1i−1后无硬件,则 hi−1=0h_{i-1}=0hi−1=0)。
实际开始时间:Sj,i=max(Rj,i,核心 core(i) 在 Rj,i 时刻的队列空闲时间)S_{j,i} = \max\left( R_{j,i}, \text{核心 } \text{core}(i) \text{ 在 } R_{j,i} \text{ 时刻的队列空闲时间} \right)Sj,i=max(Rj,i,核心 core(i) 在 Rj,i 时刻的队列空闲时间)Fj,i=Sj,i+eiF_{j,i} = S_{j,i} + e_iFj,i=Sj,i+ei
消息完成时间:Tj=Fj,PT_j = F_{j,P}Tj=Fj,P
3. 关键:如何计算"核心队列空闲时间"?
核心队列空闲时间是指:该核心在时刻 ttt之前已经排队的任务全部完成后,首次空闲的时刻。
3.1 维护每个核心的"完成时间列表"
对于核心 ccc,维护一个按开始时间排序的任务完成时间列表 :CompletionListc={(S1,F1),(S2,F2),...}\text{CompletionList}_c = \{(S_1, F_1), (S_2, F_2), ...\}CompletionListc={(S1,F1),(S2,F2),...}
其中 Fk=Sk+etaskF_k = S_k + e_{\text{task}}Fk=Sk+etask,且这些任务按 SkS_kSk递增。
当新任务在时刻 ttt到达该核心队列时,其空闲时间 为:idle_timec(t)=max(t,max(Sk,Fk)∈CompletionListcSk≥last_idleFk)\text{idle\_time}c(t) = \max\left( t, \max{\substack{(S_k, F_k) \in \text{CompletionList}_c \\ S_k \ge \text{last\_idle}}} F_k \right)idle_timec(t)=max(t,max(Sk,Fk)∈CompletionListcSk≥last_idleFk)
但更简单的方式是维护一个"当前队列尾部时间":
3.2 高效方法:维护每个核心的"队列尾部时间" QcQ_cQc
定义 QcQ_cQc为:核心 ccc在当前时刻之前所有排队任务预计完成的最晚时间。
- 初始化 :Qc=0Q_c = 0Qc=0(所有核心空闲)
- 当新任务在时刻 ttt加入核心 ccc :
S=max(t,Qc)S = \max(t, Q_c)S=max(t,Qc)
F=S+etaskF = S + e_{\text{task}}F=S+etask
Qc=F(更新队列尾部时间)Q_c = F\quad(\text{更新队列尾部时间})Qc=F(更新队列尾部时间)
这个 QcQ_cQc就是该核心的**"空闲时间"**------即新任务最早能开始的时间。
3.3 为什么这有效?
因为串行流水线 + FIFO调度,每个核心上的任务严格顺序执行 ,不存在任务穿插。队列尾部时间 QcQ_cQc就是该核心完成所有已有任务的时间,新任务只能在 QcQ_cQc之后开始。
注意 :如果有优先级抢占,QcQ_cQc需要替换为**"当前正在执行的高优先级任务完成时间"**,稍后扩展。
4. 完整的递推算法(确定性)
4.1 算法伪代码
python
# 初始化:每个核心的队列尾部时间
Q = [0] * K # K个核心
# 存储每个消息的阶段完成时间
F = {} # F[(j, i)] = 完成时间
for j in range(num_messages): # 按到达顺序处理消息
t_arr = arrival_times[j]
# 阶段1
S = max(t_arr, Q[core(1)])
F[(j, 1)] = S + e_1
Q[core(1)] = F[(j, 1)]
# 阶段2..P
for i in range(2, P+1):
# 前驱完成 + 硬件等待
R = F[(j, i-1)] + h_{i-1}
# 排队等待
S = max(R, Q[core(i)])
F[(j, i)] = S + e_i
Q[core(i)] = F[(j, i)]
# 消息完成时间
T_j = F[(j, P)]
4.2 举例说明
假设系统:
- Core 0:阶段1(5ms) + 阶段3(3ms)
- Core 1:阶段2(4ms) + 硬件(2ms)
两个消息到达:
- M1:t=0
- M2:t=2
手动计算:
| 时间 | 事件 | Core0 Q | Core1 Q |
|---|---|---|---|
| 0 | M1到达,阶段1开始 | Q0=5 | Q1=0 |
| 2 | M1阶段1完成,阶段2就绪(R=2),Core1空闲,开始阶段2 | Q0=5 | Q1=6(4+2硬件) |
| 5 | M1阶段2完成,阶段3就绪(R=5+2=7),但Core0队列尾部是5,所以S=max(7,5)=7 | Q0=10(3+7) | Q1=6 |
| 2 | M2到达,阶段1就绪,Core0队列尾部是5,S=max(2,5)=5 | Q0=10 | Q1=6 |
| 7 | M1阶段3完成,M1完成时间=7 | - | - |
| 10 | M2阶段1完成,阶段2就绪(R=10),Core1队列尾部是6,S=max(10,6)=10 | - | Q1=14(4+10) |
| 14 | M2阶段2完成,阶段3就绪(R=14+2=16),Core0队列尾部是10,S=max(16,10)=16 | Q0=19(3+16) | - |
| 19 | M2阶段3完成,M2完成时间=19 | - | - |
结论:
- M1完成时间 = 7ms
- M2完成时间 = 19ms(因为被M1阻塞)
5. 扩展:支持优先级调度
如果你的系统支持高优先级消息抢占低优先级消息,模型需要调整:
5.1 维护每个核心的"按优先级排序的完成时间树"
核心 ccc上不再是一个FIFO队列,而是按优先级排序的队列。当高优先级任务到达时,它可以插入到队列头部,导致低优先级任务延迟。
改造方案:
- 每个核心维护一个优先级队列(按优先级降序)。
- 当新任务加入时,插入到合适位置。
- 核心的"队列尾部时间" QcQ_cQc不再简单等于最后一个任务完成时间,而是所有任务按优先级顺序执行的最终完成时间。
5.2 递推公式(抢占式)
如果允许抢占(即高优先级任务到达时,立即暂停低优先级任务),则:
- 当前正在执行的低优先级任务会被打断,剩余时间放入队列。
- 高优先级任务先执行完毕。
- 继续执行低优先级任务剩余部分。
实现方式:在事件模拟中,每次新任务到达时,检查当前核心上执行的任务优先级,如果新任务优先级更高,则:
- 当前任务的剩余时间记录
- 新任务先执行
- 完成后恢复原任务
由于串行场景下每个核心同时只执行一个任务,这个逻辑实现起来很简单。
6. 数学模型 vs 事件模拟
上面的递推公式实际上就是事件驱动的确定性模拟。区别在于:
| 方法 | 实现方式 | 优点 | 缺点 |
|---|---|---|---|
| 递推公式(离线计算) | 按消息到达顺序一次性算完所有完成时间 | 快速,O(n*P) | 需要所有消息到达时间已知 |
| 事件模拟(在线计算) | 随着消息到达逐步更新Q_c | 在线,实时预测 | 每次新消息需O§计算 |
在你的场景中,消息不定期到达 ,因此推荐事件模拟(在线),每次消息到达时:
- 用当前系统快照(各核心Q_c值)
- 运行递推公式预测该消息的完成时间
- 更新系统状态(Q_c)
7. 终极简化:完全数学公式(无需循环)
对于串行流水线 ,每个消息的完成时间可以写成闭合形式:Tj=tj+∑i=1Pei+∑i=1Phi+∑i=1Pmax(0,Qcore(i)−tj−∑k=1i−1(ek+hk))T_j = t_j + \sum_{i=1}^P e_i + \sum_{i=1}^P h_i + \sum_{i=1}^P \max\left(0, Q_{\text{core}(i)} - t_j - \sum_{k=1}^{i-1} (e_k + h_k) \right)Tj=tj+∑i=1Pei+∑i=1Phi+∑i=1Pmax(0,Qcore(i)−tj−∑k=1i−1(ek+hk))
但这个公式在并发场景下变得复杂(因为Q_c本身依赖历史任务)。更实际的做法:用上述递推算法,每个消息只遍历一次流水线阶段,复杂度O§,极快。
8. 实操建议
8.1 在嵌入式RTOS中实现
c
// 每个核心维护一个"预计完成时间"
uint32_t core_completion_time[K] = {0};
// 当新消息到达时调用
uint32_t predict_completion_time(uint32_t arrival_time) {
uint32_t prev_finish = arrival_time;
for (int stage = 0; stage < P; stage++) {
int core = stage_core[stage];
uint32_t exec_time = stage_exec[stage];
uint32_t hardware_wait = stage_hw[stage]; // 本阶段后的硬件等待
uint32_t ready_time = prev_finish + hardware_wait;
uint32_t start_time = max(ready_time, core_completion_time[core]);
uint32_t finish_time = start_time + exec_time;
// 更新核心完成时间(用于后续消息)
core_completion_time[core] = finish_time;
prev_finish = finish_time;
}
return prev_finish; // 消息完成时间
}
8.2 处理"多个阶段在同一核心"的情况
如果阶段1和阶段3都在Core 0上,上述算法自动处理:
- 阶段1完成后,Core 0的Q0更新为F1。
- 阶段3就绪时(可能已经过了很久),取S = max(R, Q0),如果Q0 > R,说明Core 0还在处理其他消息,新阶段需要等待。
9. 结论
对于串行流水线 + 消息并发的场景:
- 无需机器学习,数学模型完全精确。
- 核心思想 :为每个CPU核心维护一个
队列尾部时间 Q_c,表示该核心完成所有已有任务的最晚时间。 - 递推公式 :每个消息遍历流水线阶段,用
S = max(ready_time, Q_c)计算开始时间,更新Q_c = S + exec_time。 - 时间复杂度:O§每个消息,嵌入式可实时运行。
这套模型可以精确预测每条消息的完成时间,误差为0(因为所有参数确定)。