6.4_IPIPE双内核交替调度

6.4 IPIPE: Xenomai/Linux双核交替调度

6.4.1 Xenomai/Linux线程交替调度的本质

1. 典型操作系统进程状态

进程状态转换是操作系统中非常核心的概念。典型的操作系统中的进程状态如图所示,这些状态描述了一个进程在其生命周期中的不同阶段。

为了方便记忆,可以把它想象成一个瓷碗,称之为瓷碗模型。

在这里插入图片描述

  • 创建态(New):当一个新进程刚刚被创建时,它处于创建态。此时,操作系统正在为该进程分配必要的资源和设置环境。

  • 就绪态(Ready):一旦进程已经准备好可以运行,但尚未被调度到CPU上执行,它就处于就绪态。这意味着进程已经获得了除CPU时间之外的所有必要资源,并等待着被操作系统调度以获取CPU时间。

  • 运行态(Running):当进程被分配了CPU并正在执行其指令时,它处于运行态。这是进程实际占用CPU进行计算或数据处理的状态。

  • 阻塞态(Blocked):如果进程因为需要等待某个事件发生(例如I/O操作完成)而无法继续执行,则会被置于阻塞态。在这种状态下,即使给它CPU时间,它也无法执行,直到等待的事件发生。

  • 终止态(Terminated):当进程完成了其任务或者由于某些原因被强制停止后,它将进入终止态。这时,操作系统会释放该进程所占用的所有资源,并清除相关的控制结构。

这些状态之间的转换是由操作系统内核根据特定的调度算法和系统事件来管理的。

2. Linux进程状态

抛掉创建态和终止态,我们来看一下Linux如何定义就绪态,运行态,阻塞态。

在这里插入图片描述

  • (1)TASK_RUNNING(就绪态&运行态):

就绪态&运行态都用TASK_RUNNING标记。这个表示进程要么正在CPU上执行,要么在就绪队列中排队,已经准备好执行并且等待被调度器分配CPU时间。这个地方容易混淆,可是Linux说是这么设计的。

  • (2)TASK_INTERRUPTIBLE(可中断阻塞态):

当一个进程正在等待某个事件发生(如I/O操作完成、信号量变为可用等),它会被置于TASK_INTERRUPTIBLE状态。这种状态下,尽管进程暂时不能继续执行,但它可以被信号中断。例如,如果一个进程正在等待磁盘读取完成,它可以响应来自外部的信号(如终止信号),从而提前退出等待状态。

  • (3)TASK_UNINTERRUPTIBLE(不可中断阻塞态):

类似于TASK_INTERRUPTIBLE,但处于此状态的进程不会响应任何信号,直到它所等待的系统资源变得可用。这种情况通常出现在进程需要直接访问硬件或进行关键性的系统调用时,为了避免因信号干扰导致数据损坏或其他不稳定情况。因此,使用这种状态要非常谨慎。

3. Xenomai进程状态

抛掉创建态和终止态,我们来看一下Xenomai如何定义就绪态,运行态,阻塞态。

在这里插入图片描述

  • (1)就绪态XNREADY

XNREADY代表已经加入到就绪队列,等待运行

  • (2)运行态

图中运行态不是画错了,而是根据代码理解到的真实情况,没有专门为运行态定义一个标记。

在xnsched_run中,调用xnarch_switch_to切换到新的xnthread时,会把XNREADY标记清除掉。

xnsched_run
-> __xnsched_run
-> pipeline_schedule
-> ___xnsched_run
-> next = xnsched_pick_next(sched);
    -> thread = xnsched_rt_pick(sched);//从Multi-level priority queue取出一个thread
    -> set_thread_running(sched, thread); //把XNREADY清除掉
-> pipeline_switch_to(prev, next, leaving_inband)
    -> xnarch_switch_to(prev, next);
        -> ipipe_switch_to(prev, next)
            -> switch_to(prev, next, last); //完成切换
  • (3)阻塞态

Xenomai定义的阻塞态,其中包括XNSUSP,XNPEND,XNDELAY,XNRELAX,XNHELD等情况。特别注意其中的XNRELAX,后面会用到。

XNSUSP (0x00000001): 表示线程被挂起。挂起状态意味着线程暂时不执行,直到它被恢复。

XNPEND (0x00000002): 表示线程正在等待某个资源。这类似于Linux中的TASK_INTERRUPTIBLE状态,但具体实现可能有所不同。

XNDELAY (0x00000004): 表示线程被延迟执行。可能是由于定时器或其他机制导致线程需要在未来的某个时间点才开始运行。

XNRELAX (0x00000080): 表示线程是迁移到了Linux运行,处于放松的状态。

XNHELD (0x00000200): 表示线程为了处理紧急情况而被持有。这可能是为了确保关键操作不会被打断。

4. Xenomai/Linux交替调度沙漏模型

只保留Xenomai和Linux的进程状态的就绪态、运行态、阻塞态,再把Xenomai模型上下翻转,与Linux模型就构成了一个沙漏模型!

何谓交替?就是任务在Xenomai和Linux之间可以流转,可以在Linux中以tast_struct形式被调度运行,或可以在Xenomai以xnthread形式被调度运行。就像一个沙漏一样,当沙漏被上下翻转了,沙子(任务)可以流动到下方。而IPIPE就是翻转沙漏的手!

在这里插入图片描述

  • (1)进程从Linux交替到Xenomai中运行

这个过程又称为硬化过程(可以联想到硬实时),由xnthread_harden函数实现。

它会将task_struct中的state设置为TASK_INTERRUPTIBLE状态,阻止进程在Linux中被调用。

它会将xnthread的XNRELAX标记用xnthread_resume函数清除,最终让线程将进入就绪(READY)状态

  • (2)进程从Xenomai交替到Linux中运行

这个过程又称为轻松过程(可以联想到脱离硬实时),由xnthread_relax函数实现。

它会调用xnthread_suspend函数将xnthread设置为XNRELAX,阻止xnthread在Xenomai中执行。

它会清除task_struct中的TASK_INTERRUPTIBLE状态,最终让task_struct进行就绪状态。

后面会展开xnthread_harden函数和xnthread_relax函数具体实现的分析。

6.4.2 Xenomai xnthread_harden原理

xnthread_harden 函数主要用于把线程从root domain迁移到head domain,以此满足实时任务对低延迟响应的需求。

xnthread_harden 函数最典型的调用场景:

(1)创建xnthread线程后,会执行xnthread_harden切换线程到head domain,参考《6.1 Xenomai进程的创建流程》和《6.2 Xenomai线程的创建流程》。

(2)就是当执行xenomai调用,必须执行xnthread_harden确保线程线切换到head domain,参考《5.4 IPIPE: Xenomai/Linux双核系统调用》。

xnthread_harden中调用的最核心的函数是第1938行的pipeline_leave_inband()函数。

在这里插入图片描述

抽取其中最主要的调用框架,得到如下函数调用堆栈,接下来分3大部分逐步解释其中的玄机。图中用3种颜色来区分3大步骤。

在这里插入图片描述

1. set_current_state(TASK_INTERRUPTIBLE | TASK_HARDENING)

在这里插入图片描述

__ipipe_migrate_head调用 set_current_state 函数,将当前任务task_stuct的state状态更新为 TASK_INTERRUPTIBLE 和 TASK_HARDENING 的组合状态。

TASK_INTERRUPTIBLE:这是一个任务状态标志,表示任务处于可中断的睡眠状态。

TASK_HARDENING:是Xenomai为task_struct引入的自定义的任务状态标志,用于标记任务正处于从普通状态向实时状态转换的过程。

2. __schedule(false)

在这里插入图片描述

__schedule()是 Linux 内核中核心的调度函数,负责进行任务的切换和调度。它会调用pick_next_task,根据调度算法从运行队列中选择一个合适的任务,执行context_switch将 CPU 控制权交给该任务。

在这里插入图片描述

现在的场景是要把一个任务从Linux切换到Xenomai,为什么__ipipe_migrate_head调用__schedule()来切换Linux的任务呢?

这是有道理的。

当前任务”小沙”占用着CPU,处于执行态。想把它的执行上下文从Linux直接变成Xenoami,非常困难。就好像你不能边开飞机边修飞机吧。

所以,先在root domain,用__schedule函数把当前任务调度出去,重新在Linux的任务队列中拿到一个新的任务“小漏“并执行。

这样方便对任务“小沙“继续操作。具体是怎么操作的呢?接着往下看一下context_switch函数的细节。

在这里插入图片描述

context_switch函数非常复杂,这里就不逐行去解释了,而是抓住最核心两个函数:switch_to和__ipipe_switch_tail。

1) switch_to

switch_to 是一个关键的上下文切换函数,在 Linux 内核中用于实现任务上下文的切换。prev 是当前正在执行的任务(即将被切换出去的任务),我们还是用“小沙”作为代称。next 是即将要执行的新任务,我们还是用“小漏”作为代称。

从switch_to函数返回时,已经完成从当前任务 prev 到新任务 next 的上下文切换,包括寄存器状态、栈指针等的切换,使得 CPU 开始执行新任务 next 的代码。这段描述可能有点让人费解,稍微解释一下。

先想一下,next任务“小漏”曾经是处于执行态的。当“小漏”被调度出去时,也是走到了第2838行的switch_to函数,所以“小漏”的任务上下文中,它就停留在switch_to函数返回的位置。

然后,一旦当前任务“小沙”被调度出去,把“小漏”调度进来,那么“小漏”还是从switch_to函数返回的位置之后开始向下执行。

最后,需要注意,任务“小沙”是一体两面的,在Linux有自己的调度实体task_struct,同时在Xenomai有自己的影子线程xnthread。

switch_to函数只是完成了对task_struct的操作,而对xnthread的操作,就依赖第2841行的Xenomai插入的__ipipe_switch_tail函数。

2) __ipipe_switch_tail

__ipipe_switch_tail这个函数已经处于任务“小漏”的上下文中了,而任务“小沙”已经处于TASK_INTERRUPTIBLE可中断睡眠状态,此时就时对任务“小沙”进行精准的外科手术。任务“小沙”是一体两面的,在Linux有自己的调度实体task_struct,同时在Xenomai有自己的影子线程xnthread。__ipipe_switch_tail操作的对象必然是xnthread!

__ipipe_switch_tail具体应该怎么操作?

它必须调用IPIPE定义的接口函数complete_domain_migration!

3. complete_domain_migration函数

1)t->state &= ~TASK_HARDENING;

将task_stuct的state状态中的TASK_HARDENING标记清除。这个标记是__ipipe_migrate_head调用 set_current_state 函数设置的。这也说明TASK_HARDENING标记表达的就是一个过程状态。

2)ipipe_set_ti_thread_flag(task_thread_info(t), TIP_HEAD);

参考《5.3.2 ipipe_flags之TIP_HEAD》,IPIPE在struct thread_info中新增了ipipe_flags,用于Xenomai/Linux双内核之间进行交互。其中TIP_HEAD代表进程当前运行在Head域。

3)ipipe_migration_hook(t);

在这里插入图片描述

第177行,xnthread_resume(thread, XNRELAX)

xnthread_resume 函数用于恢复之前通过调用 xnthread_suspend() 而被挂起的线程。此函数通过移除影响目标线程的一个或多个挂起条件来实现这一点。当所有挂起条件都被移除后,线程将进入就绪(READY)状态,并重新具备调度资格。

传入的第一个参数thread,就是一个xnthread指针:struct xnthread *thread = xnthread_from_task§。它就指向任务“小沙“在Xenomai的影子线程xnthread。

传入的第二参数XNRELAX,就表示要清除struct xnthread 中的XNRELAX阻塞状态,最终设置XNREADY就绪状态。

第194行,xnsched_run

xnsched_run会执行调度操作。

调用xnsched_pick_next(sched)从调度队列中选中一个处于XNREADY的xnthread,假设此时“小沙“xnthread排在了第一,被选中了。

调用pipeline_switch_to,把选中的“小沙“xnthread调度到CPU上执行,把屁股还没坐热的”小漏“给挤下去了。

有趣的事情是,xnsched_run最终是复用了Linux的switch_to函数完成任务切换。

xnsched_run
-> __xnsched_run
-> pipeline_schedule
-> ___xnsched_run
-> next = xnsched_pick_next(sched);
    -> thread = xnsched_rt_pick(sched);//从Multi-level priority queue取出一个thread
    -> set_thread_running(sched, thread); //把XNREADY清除掉
-> pipeline_switch_to(prev, next, leaving_inband)
    -> xnarch_switch_to(prev, next);
        -> ipipe_switch_to(prev, next)
            -> switch_to(prev, next, last); //完成切换

小结一下:

为了把任务“小沙“从Linux送到Xenomai,xnthread_harden首先在Linux中,把任务”小沙“的tast_struct调度出去,换成了任务”小漏“。

在任务“小漏”屁股还没坐热的时候,把任务“小沙”在Xenomai的影子线程xnthread调到到CPU运行了,把任务“小漏”从CPU上挤下去了。

而且,是在任务“小漏“执行过程中,自己把自己给挤下去了,有点不讲武德哈!

6.4.3 Xenomai xnthread_relax原理

实时线程有时需要离开head域,转而在root域中执行,主要场景有两个:

(1)执行非实时工作:运行涉及由Linux内核处理的常规系统调用的非时间关键(带内)工作。

(2)处理CPU异常:从CPU异常中恢复,例如处理主要内存访问故障,对于这类故障,无需关注响应时间,也无需在实时核心中重复处理。

xnthread_relax,顾名思义,是一个放松过程(可以联想到脱离硬实时)。它把任务从Xenomai迁移到Linux中共分为4个步骤,核心调用堆栈如下,用4种颜色标识。

在这里插入图片描述

1. pipeline_post_inband_work(&wakework)

pipeline_post_inband_work是一个宏定义,它直接调用ipipe_post_work_root。

关于ipipe_post_work_root,回到第三章,参考《3.4.4 __ipipe_init_early之再论虚拟中断》,它调用__ipipe_set_irq_pending(&ipipe_root, irq)把__ipipe_work_virq虚拟中断记录到root domain的interrupt log。直到interrupt log被回放,在root domain中调用虚拟中断响应程序__ipipe_do_work来完成工作项。

通过虚拟中断__ipipe_work_virq,给root domain传递任意的工作项struct ipipe_work_header,定义如下。

include/linux/ipipe.h:

struct ipipe_work_header {
	size_t size;
	void (*handler)(struct ipipe_work_header *work);
};

pipeline_post_inband_work(&wakework)传入的参数为wakework,其定义如下。最终在root域中的回调函数是lostage_task_wakeup。

    struct lostage_wakeup wakework = {
        .inband_work = PIPELINE_INBAND_WORK_INITIALIZER(wakework,
                    lostage_task_wakeup),
        .task = p,
    };

struct lostage_wakeup {
    struct pipeline_inband_work inband_work; /* Must be first. */
    struct task_struct *task;
};

struct pipeline_inband_work {
    struct ipipe_work_header work;
};

pipeline_post_inband_work(&wakework)虽然向root domain发送了一个任务,但是当前Xenomai线程正在xnthread_relax函数上下文中运行呢,此时root domain是不可能执行此任务的。

2. xnthread_suspend(thread, suspension, XN_INFINITE, XN_RELATIVE, NULL)

结合上下文,可知传入的第二参数suspension,必然在上一个函数pipeline_leave_oob_prepare()中置位了XNRELAX标记。

在这里插入图片描述

第943~945行,xnthread_clear_state(thread, XNREADY)清除xnthread的XNREADY标识。

第960行,xnsched_set_resched(sched)设置调度器的重新调度标志,表示需要进行重新调度。

第970,__xnsched_run(sched) 执行调度器的重新调度操作,从当前任务“小沙”的xnthread切换到新的xnthread线程“小飞”。

至此,当前任务”小沙”的xnthread会一直处于XNRELAX阻塞状态,不可能被Xenomai调度器调度了,就像被按下了暂停键。而任务“小沙”是一体两面的,它在Linux中的task_struct还处于TASK_INTERRUPTIBLE可中断睡眠状态中,怎么唤醒它呢?

还记得第1步中向root域发送的任务wakework吗?当Xenomai所在的head域空闲的时候,会执行root域的中断回放,就会回调任务wakework中定义的lostage_task_wakeup函数。

3. lostage_task_wakeup

当lostage_task_wakeup函数被回调时,它处于root域中断回放的上下文中。它的核心就是调用wake_up_process§来唤醒指定的进程,其参数p就指向任务“小沙”的task_struct。

在这里插入图片描述

wake_up_process 是 Linux 内核中用于唤醒休眠进程的关键函数,其核心逻辑如下:

(1)状态转换

将目标进程从休眠状态(如 TASK_INTERRUPTIBLE 或 TASK_UNINTERRUPTIBLE)切换到可运行状态(TASK_RUNNING),使其有资格被调度器选中执行。

(2)加入调度队列

将进程添加到 CPU 的运行队列(runqueue)中。

(3)抢占CPU并运行

如果被唤醒的进程和当前进程属于相同的调度类,那么调度调度类的check_preempt_curr方法以检查是否可以抢占当前进程。

如果被唤醒的进程所属调度类的优先级高于当前进程所属调度类的优先级,那么给当前进程设置需要重新调度的标志。

4. pipeline_leave_oob_finish

xnthread_relax得以从第2步暂停的地方,继续运行,从xnthread_suspend函数返回。注意此时是task_struct在root domain中运行。

接下来走到pipeline_leave_oob_finish,它调用__ipipe_reenter_root()来完成最后的收尾工作。

(1)ipipe_clear_thread_flag(TIP_HEAD);

参考《5.3.2 ipipe_flags之TIP_HEAD》,IPIPE在struct thread_info中新增了ipipe_flags,用于Xenomai/Linux双内核之间进行交互。其中TIP_HEAD代表进程当前运行在Head域。

(2)finish_task_switch(p)

该函数的主要功能是在任务切换完成后,进行一些必要的清理和更新操作,确保系统状态的一致性和正确性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值