编写LitmusRT调度器插件

目录

背景

打桩

编译安装

引入TRACE模块来输出debug信息

为P-EDF定义每个CPU的状态

激活插件

模块测试

添加调度逻辑

帮助函数

demo_job_completion()

demo_requeue()

定义P-EDF调度逻辑

任务状态改变

新任务处理

任务退出

恢复挂起任务

加入抢占检查

回调注册

修改1:挂起任务恢复

修改2:新任务处理

接受实时任务

在/proc中暴露插件结构

注册/proc状态

销毁/proc状态

回调注册

测试


背景

LitmusRT是一个基于Linux的实时调度平台,其提供了一些实现了不同实时调度算法的调度器插件以供我们进行实验,也暴露了一些接口以让我们实现自己的调度插件,本文记录一下在LitmusRT上使用LitmusRT暴露的API实现自定义调度插件的过程,参考文档Writing a LITMUSRT Scheduler Plugin

这一切,需要事先编译安装好LitmusRT内核,参见官方文档Installing LITMUSRT

首先把工作目切换到litmusRT根目录下的litmus目录

root@ubuntu:/home/szc# cd litmus/litmus-rt/litmus/

我们将自定义调度器插件的名字命名为demo(实现的是简单的P-EDF算法),以下是详细编写、编译和测试过程

打桩

编写demo文件sched.demo.c,内容如下

#include <linux/module.h>
#include <litmus/preempt.h>
#include <litmus/sched_plugin.h>

static struct task_struct* demo_schedule(struct task_struct* prev) {
    sched_state_task_picked();
    return NULL;
}

static long demo_admit_task(struct task_struct* task) {
    return -EINVAL;
}


static struct sched_plugin demo_plugin = {
    .plugin_name = "demo",
    .schedule    = demo_schedule,
    .admit_task  = demo_admit_task,
};

static int __init init_demo(void) {
    return register_sched_plugin(&demo_plugin);
}

module_init(init_demo);

litmusRT中所有的调度插件都是linux内核模块,需要一个初始化函数。而文件最底部的入口函数modue_init(),就是用来告诉编译器和链接器在初始化时要执行的是哪个函数,这里就是上面的init_demo()。这个函数返回值后面的__init用来告诉编译器,把这个函数放在一块定义在 vmlinux.lds特殊的区域中,以便在这个函数执行一次后,就把它占用的内存释放。

在我们的init_demo()函数体中,是我们的初始化逻辑。很简单,就是调用litmusRT函数register_sched_plugin(),顾名思义,就是把调度器插件进行注册。入参是一个sched_plugin结构体指针,返回值为0就是成功。

我们的sched_plugin结构体名为demo_plugin,内部对三个变量进行赋值:plugin_name自然就是我们调度器的名字,schedule和admit_task用来指定当调度事件任务提交事件发生时的回调函数。当然sched_plugin结构体内还有别的函数,只是默认都是空实现。

那么我们任务提交的回调函数就是demo_admit_task(),当使用我们的demo调度器时,只要有实时任务提交,就会触发这个函数,入参显然就是被提交的这个任务的指针。此处,我们什么也不做,就返回一个参数错误,做测试。

最后是我们的调度任务回调函数,当使用我们的demo调度器时且有任何实时任务被调度时,就会调用这个函数。入参就是要被调度的任务指针,如果我们要调度这个任务的话,就把这个指针返回出去。此处我们不想调度(因为任务在提交那里就被卡住了),所以就返回了个空指针,以允许默认的linux调度器调度非实时任务。但是无论如何,我们都要调用sched_state_task_picked()函数通知litmusRT内核,说我们已经做了一个调度决策。

编译安装

写完sched_demo.c文件还不算,我们要把要链接的文件名添加到litmus目录中的MakeFile中,在obj-y     =...的=后面一长串里,加上我们的sche_demo.o即可

        ...........
        sched_pfp.o \
        sched_demo.o

然后回到litmusRT根目录,编译安装之。为了省事儿,我做了一个脚本,名曰compile_install.sh,里面就是那几个编译安装命令

make bzImage -j6 && make modules -j6
sudo make modules_install -j6
sudo make install -j6

执行命令时,就可以看到我们的.o文件被链接了

安装完后,重启系统。开机之后,查看可用的调度器,里面赫然有我们的demo

root@ubuntu:/home/szc# cat /proc/litmus/plugins/loaded
P-RES
PFAIR
C-EDF
demo
P-FP
PSN-EDF
GSN-EDF
Linux

 然后使能它,并查看

root@ubuntu:/home/szc# setsched demo
root@ubuntu:/home/szc# showsched
demo

最后调度一个任务(rtspin命令的使用参见文章LitmusRT使用笔记),不出意外会报错参数非法

root@ubuntu:/home/szc# rtspin 10 100 10
could not become RT task: Invalid argument

这是符合我们代码逻辑的,至此,第一个调度器demo就成功编译到litmusRT内核中了

引入TRACE模块来输出debug信息

在标准的linux内核的大多数模块中,printk是一个把调试信息输出到内核日志的首选,但是这需要一些内核时间,因此如果我们的litmusRT插件在做调度决策时使用它来输出信息就可能会导致死锁。litmusRT有自己的TRACE模块取而代之,这个模块的作用和printk等同但不要求锁。

我们使用时,要添加头文件

#include <litmus/debug_trace.h>

然后在提交任务的回调函数中利用TRACE模块输出一行调试信息

static long demo_admit_task(struct task_struct* task) {
    TRACE_TASK(task, "The task was rejected by demo plugin!\n");
    return -EINVAL;
}

TRACE_TASK()函数的入参有二:任务指针和要输出的信息,那么它就会输出我们指定的信息和这个任务的一些信息。

为了让TRACE模块工作,我们要确定内核配置项 TRACE() debugging 开启了(在 LITMUS^RT->Tracing 下)。对litmusRT内核重新编译安装后,重启系统(如果安装了英伟达驱动,可能重启后要重新运行英伟达的run文件,然后再重启一次)。然后配置调度器,把/dev/litmus/log文件追加到debug.txt下,启动实时任务,查看debug日志

root@ubuntu:/home/szc# setsched demo
root@ubuntu:/home/szc# cat /dev/litmus/log > debug.txt &
[1] 9082
root@ubuntu:/home/szc# rtspin 10 100 0.1
could not become RT task: Invalid argument
root@ubuntu:/home/szc# cat debug.txt

结果如下:

可以看到The task was rejected by demo plugin!这句话被输出了

为P-EDF定义每个CPU的状态

首先要引入一些头文件:

#include <linux/percpu.h> 
#include <linux/sched.h> 
#include <litmus/litmus.h> 
#include <litmus/rt_domain.h> 
#include <litmus/edf_common.h>

cpu的本地分配会用到linux/percpu.h;struct task_struct结构体在linux/sched.h中定义;litmus/litmus.h中定义了LitmusRT中一些所需的结构;litmus/rt_domain.h包含了和实时领域相关的定义,以及就绪队列和释放队列的定义;litmus/edf_common.h包含了和EDF优先级以及相关的帮助类函数的定义

为了实现P-EDF调度器,我们需要在每个处理器上都有一个调度队列和一个释放队列,对此我们将使用litmus/rt_domain.h头文件中的rt_domain_t来处理。而且,我们还需要知道当前调度的是那个任务,为了方便,我们会把每个CPU的ID保存到其本地状态中(这是为了下一步的抢占更加容易),所以我们需要在头文件下面定义demo_cpu_state结构体,用来保存所有的CPU状态:

struct demo_cpu_state {
    rt_domain_t local_queues;
    int cpu;
    struct task_struct* scheduled;
};

static DEFINE_PER_CPU(struct demo_cpu_state, demo_cpu_state);

#define cpu_state_for(cpu_id) (&per_cpu(demo_cpu_state, cpu_id))
#define local_cpu_state() (this_cpu_ptr(&demo_cpu_state))

注意我们使用了Linux的DEFINE_PER_CPU宏来静态地为插件分配需要状态。而且,为了让代码更加可读,我们也定义了两个处理器宏来包装Linux的cpu数据结构API:cpu_state_for()和local_cpu_state()

为了确保每个处理器的状态被正确初始化了,我们下一步要加入插件激活回调函数。插件的activate_plugin()回调将会在插件被用户选择(setsched)时被调用,因此可以用来进行插件初始化工作。

激活插件

在demo插件中,我们使用edf_domain_init()来初始化就绪队列和释放队列,再初始化cpu和scheduled字段,在sche_plugin结构体定义之前加入以下代码:

static long demo_activate_plugin(void) {
    int cpu;
    struct demo_cpu_state *state;

    for_each_online_cpu(cpu) {
        TRACE("Initializing CPU%d....\n", cpu);

        state = cpu_state_for(cpu);
        state->cpu = cpu;
        state->scheduled = NULL;
        edf_domain_init(&state->local_queues, NULL, NULL);
    }

    return 0;
}

然后在demo_plugin结构体中声明要使用的激活回调函数:

static struct sched_plugin demo_plugin = {
    .plugin_name = "demo",
    .schedule    = demo_schedule,
    .admit_task  = demo_admit_task,
    .activate_plugin = demo_activate_plugin,
};

模块测试

重新编译安装litmus内核并重启后,可以进行检验:

root@rtlab-computer:/home/rtlab# cat /dev/litmus/log > debug.txt &
root@rtlab-computer:/home/rtlab# setsched demo
root@rtlab-computer:/home/rtlab# cat debug.txt

内容如下:

添加调度逻辑

先加入这两个头文件:

#include <litmus/jobs.h>
#include <litmus/budget.h>

帮助函数

当我们在调度器中管理作业和预算相关的信息时,会用到这两个头文件。然后我们写两个帮助函数:

demo_job_completion()

用来当作业完成后进行一些善后工作。在这个例子里,我们只是在里面调用于litmus/jobs.h中声明的prepare_for_next_period(),来计算下一次作业的释放时间、截止日期等:

static void demo_job_completion(struct task_struct *prev, int budget_exhausted) {
    prepare_for_next_period(prev);
}

demo_requeue()

用来把任务放到合适的队列中。如果任务有挂起作业,那么任务就会被放到cpu中的就绪队列中;否则,如果其下一个作业的释放时间在未来,那么任务会被放到cpu的释放队列中:

static void demo_requeue(struct task_struct *tsk, struct demo_cpu_state *cpu_state) {
    if (is_released(tsk, litmus_clock())) {
        __add_ready(&cpu_state->local_queues, tsk);
    } else {
        add_release(&cpu_state->local_queues, tsk);
    }
}

在这里,我们调用了__add_ready()是因为这个函数只会在调用线程已经有了就绪队列的锁之后被调用,但是不会拥有释放队列的锁,因此不会调用__add_release()

定义P-EDF调度逻辑

接下来就是重点了——定义P-EDF调度逻辑,总体上说,分为三步

  1. 检查之前调度的实时任务(如果有的话)的状态;
  2. 检查就绪队列中是否存在更高优先级的任务(也就是是否需要抢占);
  3. 返回需要被调度下一个任务(如果没有挂起的实时任务,就返回null)。

对于每个处理器来说,调度器都是串行的,因为使用了就绪队列锁。复用就绪队列锁来串行化调度决策是litmus中的惯例,或者我们也可以在demo_cpu_state中加一个额外的自旋锁。修改后的demo_schedule函数如下:

static struct task_struct* demo_schedule(struct task_struct * prev) {
    struct demo_cpu_state *local_state = local_cpu_state(); // 当前CPU状态,主要是得到里面的前驱任务(scheduled字段)
    
    struct task_struct *next = NULL; // next指针为null表示调度后台任务

    int exists, out_of_time, job_completed, self_suspends, preempt, resched; // 前驱任务状态

    raw_spin_lock(&local_state->local_queues.ready_lock); // 持有当前CPU就绪队列的自旋锁

    BUG_ON(local_state->scheduled && local_state->scheduled != prev); // 确保local_state中的scheduled字段指向了prev参数
    BUG_ON(local_state->scheduled && !is_realtime(prev)); // 确保prev是实时任务

    exists = local_state->scheduled != NULL; // 是否存在前驱
    self_suspends = exists && !is_current_running(); // 前驱是否挂起
    out_of_time = exists && budget_enforced(prev) && budget_exhausted(prev); // 前驱是否超时(超出预算)
    job_completed = exists && is_completed(prev); // 前驱是否完成
    
    preempt = edf_preemption_needed(&local_state->local_queues, prev); // 如果就绪队列中存在比前驱任务优先级更高的任务,就进行抢占
    
    resched = preempt; // 检查重调度的所有条件
    
    if (self_suspends) { // 如果前驱挂起,不会对其重调度
        resched = 1;
    }
    
    if (out_of_time || job_completed) { // 如果前驱超时或者完成了,也不会重调度
        demo_job_completion(prev, out_of_time);
        resched = 1;
    }

    if (resched) {
        // 如果此时不打算对前驱重调度,并且前驱不是挂起(而是完成或者超时),就将其再次入队
        if (exists && !self_suspends) {
            demo_requeue(prev, local_state);
        }
        next = __take_ready(&local_state->local_queues); // 取出就绪队列中的就绪任务,做为后继
    } else {
        // 如果打算重调度前驱,后继就是前驱
        next = local_state->scheduled;
    }

    local_state->scheduled = next; // 指定要调度的任务为后继
    if (exists && prev != next) { // 如果后继不是前驱,那就是要调度新的任务
        TRACE_TASK(prev, "descheduled.\n");
    }
    if (next) {
        TRACE_TASK(next, "scheduled.\n");
    }
    
    sched_state_task_picked(); // 告诉litmus内核,我们已经做了一个调度决策

    raw_spin_unlock(&local_state->local_queues.ready_lock); // 释放锁
    return next;
}

通过调用raw_spin_lock(&local_state->local_queues.ready_lock)函数获取就绪队列锁来串行化调度器,注意当调度器的schedule()回调被调用时中断是关闭的,因此我们不必担心demo_schedule()函数里面会发生本地中断。

随后两行BUG_ON用来保证两点:1)、当CPU调度实时任务时,此任务是被local_state->scheduled指针指向的;如果没有调度实时任务,那么scheduled指针应该为空。注意,prev形参可能指向一个非实时任务。

接下来的几行用来标识prev的状态,如下表所示:

变量名

真时的意义

exists

前驱任务为实时任务

self_suspend

前驱任务再也不会被调度

out_of_time

前驱任务运行超过了预算

job_completed

前驱任务通过系统调用发送了完成信号

注意,这里的前驱任务就是当前正在被调度任务。下面的edf_preemption_needed()检查是否有高优先级的任务(比如截止日期更早)在本地就绪队列中挂起等待。而后,我们检查是否需要抢占或者调度。如果前驱任务没有挂起、超时或完成,并且没有更高优先级任务等待的话,那么要调度的任务(后继)就还是前驱。注意这一步包含了对demo_job_completion()的调用,它只是对prepare_for_next_period()的封装,如前所述。

最后的几行实施了实际的调度决策,如果前驱需要被抢占(if (resched)分支),那么它就会调用demo_requeue()函数入队(需要的话),然后从就绪队列中获取一个新任务作为后继。否则,后继就是前驱local_state->scheduled(可能为null)。本地状态常量通过local_state->scheduled = next被next获取。

当写完这些代码后,litmus可以被编译安装重启,但是任务依旧不能被调度,因为所有的任务都被拒绝了。在接受任务之前,我们需要添加对任务状态改变的支持。

任务状态改变

新任务处理

当一个任务变成实时任务后就被称为新任务,新任务可以正在运行的或者被挂起。如果实时任务初始化自己,它应该在通知我们的调度器之前就开始运行了。另一方面,被独立的初始化任务初始化的实时任务即便处于挂起状态,也是新的任务。

不管怎样,关于这个任务的调度器状态必须被正确初始化:作业的释放时间和截止时间必须被设置;正在运行的任务必须被进一步记录到本地demo_cpu_state实例的scheduled字段中;运行队列中没有运行的任务必须被加入到就绪队列中。根据这些要求,我们可以写出demo_task_new()函数,如下所示:

static void demo_task_new(struct task_struct *tsk, int on_runqueue,  int is_running) {
    unsigned long flags;
    struct demo_cpu_state *state = cpu_state_for(get_partition(tsk)); // tsk所在的CPU状态
    lt_t now;

    TRACE_TASK(tsk, "is a new RT task %llu (on runqueue:%d, running:%d)\n", litmus_clock(), on_runqueue, is_running);
    
    raw_spin_lock_irqsave(&state->local_queues.ready_lock, flags); // 获取锁,保护状态并关闭中断

    now = litmus_clock();
    
    release_at(tsk, now); // 释放新来的任务

    if (is_running) { // 如果刚释放的任务正在运行,那么别的任务就不会运行了
        BUG_ON(state->scheduled != NULL);
        state->scheduled = tsk;
    } else if (on_runqueue) {
        demo_requeue(tsk, state); // 否则入队
    }

    raw_spin_unlock_irqrestore(&state->local_queues.ready_lock, flags); // 释放锁
}

任务退出

我们再添加一个demo_task_exit()函数,来在任务退出时获得调度器状态,此函数的大部分代码和demo_task_new()函数的一致:

static void demo_task_exit(struct task_struct *tsk) {
    unsigned long flags;
    struct demo_cpu_state *state = cpu_state_for(get_partition(tsk));
    raw_spin_lock_irqsave(&state->local_queues.ready_lock, flags);

    // 为了方便,我们假设任务不会再次入队了,这对应任务自己退出的情况;如果任务是被其他任务强行退出实时模式的,就要进行额外的队列管理
    if (state->scheduled == tsk) {
        state->scheduled = NULL;
    }

    raw_spin_unlock_irqrestore(&state->local_queues.ready_lock, flags);
}

注意,为了让例子简单,上面的demo_task_exit()函数实现没有处理一个任务解除就绪队列中另一个任务的实时状态的情况。鲁棒性更好的实现可以通过检查当前任务是否依旧存在于某些队列中来处理这种情况。

恢复挂起任务

要恢复(resume)的任务必须被再次加入到就绪或释放队列中,这取决于它的恢复时间。另外,被挂起了很长时间的随机(sporadic)任务被认为经历了一个随机任务释放,从而必须重新指定预算和截止日期。根据这部分逻辑,我们可以写出demo_task_resume()函数,如下所示:

static void demo_task_resume(struct task_struct  *tsk) {
    unsigned long flags;
    struct demo_cpu_state *state = cpu_state_for(get_partition(tsk));
    lt_t now;
    TRACE_TASK(tsk, "woke up at %llu\n", litmus_clock());
    raw_spin_lock_irqsave(&state->local_queues.ready_lock, flags);

    now = litmus_clock();

    if (is_sporadic(tsk) && is_tardy(tsk, now)) {
        // 此随机任务离开太久了,已经过了它的截止日期。因此通过触发作业释放来给它指定一个新的预算
        release_at(tsk, now);
    }
    
    if (state->scheduled != tsk) { // 这个检查是为了避免和一些任务竞争,这些任务在调度器注意到它们被唤醒前就已经恢复执行了。也就是说,唤醒可能和schedule()函数的调用产生竞争
        demo_requeue(tsk, state);
    }

    raw_spin_unlock_irqrestore(&state->local_queues.ready_lock, flags);
}

这里有两点需要注意:1)、在*state = cpu_state_for(...)中,注意处理唤醒的CPU不一定是任务分配的CPU,这是因为任务经常被中断恢复,而中断可以在任何CPU中被处理;2)、注意tsk可能正在被调度,所以我们使用if (state->scheduled != tsk)来检查。当任务刚初始化了自我挂起就发生了唤醒时,这种情况就出现了,这样可以允许在别的CPU上发生的唤醒来和挂起竞争,并在当前CPU处理完自我挂起前处理唤醒,如下图所示

最后,更新sched_plugin结构体,来加入新的回调函数。我们还需要加入一个回调来响应任务完成的时间(如果使用Feather-trace的话就要添加了),幸运的是,默认的作业完成回调——complete_job已经在jobs.h中被定义,所以我们只需要把它加入即可:

static struct sched_plugin demo_plugin = {
        .plugin_name            = "DEMO",
        .schedule               = demo_schedule,
        .task_wake_up           = demo_task_resume,
        .admit_task             = demo_admit_task,
        .task_new               = demo_task_new,
        .task_exit              = demo_task_exit,
        .activate_plugin        = demo_activate_plugin,
        .complete_job           = complete_job,
};

现在可以再次编译安装litmus了,但它依旧不会接受任务。下一步我们会开启抢占式调度。

加入抢占检查

当任务从释放队列转移到就绪队列中时,抢占检查回调就会被rt_domain_t代码调用,所以此时调用线程已经持有了就绪队列锁。必要的逻辑如下所示,此函数应该被加入到demo_activate_plugin()函数之前:

static int demo_check_for_preemption_on_release(rt_domain_t *local_queues) {
    struct demo_cpu_state *state = container_of(local_queues, struct demo_cpu_state, local_queues);

    if (edf_preemption_needed(local_queues, state->scheduled)) {
        preempt_if_preemptable(state->scheduled, state->cpu);
        return 1;
    }
    return 0;
}

抢占检查首先使用Linux标准宏container_of()从rt_domain_t指针中抽取其中的demo_cpu_state结构体,然后检查就绪队列中是否包含优先级比当前调度任务高的任务(比如截止日期更早)。如果存在这样的任务,调度器就会触发对preempt_if_preemptable()函数的调用,这个litmus帮助函数是对linux抢占机制的封装,可以在当前CPU和其他CPU上工作。

注意state->scheduled可能是空,但这种情况已经被preempt_if_preemptable()处理了。另外此函数的if_preemptable后缀是对不可抢占部分的支持,但与本教程无关。

回调注册

抢占检查回调必须在插件初始化时被传入到edf_domain_init()函数中,所以我们要用下面的方式修改demo_activate_plugin()函数:

static long demo_activate_plugin(void) {
    int cpu;
    struct demo_cpu_state *state;

    for_each_online_cpu(cpu) {
        TRACE("Initializing CPU%d...\n", cpu);
        state = cpu_state_for(cpu);
        state->cpu = cpu;
        state->scheduled = NULL;
        edf_domain_init(&state->local_queues,
                        demo_check_for_preemption_on_release, // 指定抢占检查回调
                        NULL);
    }
    return 0;
}

修改1:挂起任务恢复

当就绪队列因为唤醒任务或加入新任务而发生改变时就需要进行额外的抢占检查,例如,当一个高优先级任务被唤醒时,如果当前被调度的任务优先级低或者压根儿没有调度实时任务的话,demo_schedule()就应该被立刻调用。为了保证需要时会调用调度器,我们对demo_task_resume()函数进行了如下修改,加入明确的抢占检查:

static void demo_task_resume(struct task_struct  *tsk) {
    unsigned long flags;
    struct demo_cpu_state *state = cpu_state_for(get_partition(tsk));
    lt_t now;
    TRACE_TASK(tsk, "woke up at %llu\n", litmus_clock());
    raw_spin_lock_irqsave(&state->local_queues.ready_lock, flags);

    now = litmus_clock();

    if (is_sporadic(tsk) && is_tardy(tsk, now)) {
        release_at(tsk, now);
    }

    if (state->scheduled != tsk) {
        demo_requeue(tsk, state);
        if (edf_preemption_needed(&state->local_queues, state->scheduled)) { // 抢占检查
            preempt_if_preemptable(state->scheduled, state->cpu);
        }
    }

    raw_spin_unlock_irqrestore(&state->local_queues.ready_lock, flags);
}

修改2:新任务处理

我们也要对demo_task_new()函数做类似的修改:

static void demo_task_new(struct task_struct *tsk, int on_runqueue,  int is_running) {
    unsigned long flags;
    struct demo_cpu_state *state = cpu_state_for(get_partition(tsk));
    lt_t now;

    TRACE_TASK(tsk, "is a new RT task %llu (on runqueue:%d, running:%d)\n",
               litmus_clock(), on_runqueue, is_running);

    raw_spin_lock_irqsave(&state->local_queues.ready_lock, flags);

    now = litmus_clock();

    release_at(tsk, now);

    if (is_running) {
        BUG_ON(state->scheduled != NULL);
        state->scheduled = tsk;
    } else if (on_runqueue) {
        demo_requeue(tsk, state);
    }

    if (edf_preemption_needed(&state->local_queues, state->scheduled)) { // 抢占检查
        preempt_if_preemptable(state->scheduled, state->cpu);
    }

    raw_spin_unlock_irqrestore(&state->local_queues.ready_lock, flags);
}

最后编译一下,通过即可,下一步就是接受实时任务了

接受实时任务

插件可用之前的最后一步就是调度实时任务,我们将要修改demo_admit_task()回调来接收实时任务,只要它们位于正确的核上。修改后的demo_admit_task()如下所示:

static long demo_admit_task(struct task_struct *tsk) {
    if (task_cpu(tsk) == get_partition(tsk)) {
        TRACE_TASK(tsk, "accepted by demo plugin.\n");
        return 0;
    }
    return -EINVAL;
}

由于demo调度器实现了分区调度,我们要求实时任务在成为实时任务前已经被移植到了合适的CPU核上,这个限制极大地简化了插件的实现,而且在用户空间中也不难被支持(liblitmus包含了一个名为be_migrate_to_domain()的帮助函数来迎合这个要求)。

注意task_cpu()是linux用来查看任务当前所在的CPU的函数,而get_partition()则是litmus的用来查看任务在逻辑上被分配到哪个CPU的函数。现在这个插件在功能上就独立了,但liblitmus包含用来更简单地把任务迁移到正确的处理器(或者处理器集群)的代码,而这些代码需要插件提供额外的信息,所以下一步就是让插件提供这些信息

在/proc中暴露插件结构

注册/proc状态

为了访问litmus结构隐式API,插件需要包含litmus/litmus_proc.h头文件:

#include <litmus/litmus_proc.h>

然后,分配一个domain_proc_info类型的结构体,它会保存需要的结构信息。在sched_demo.c开头部分添加下面一行:

static struct domain_proc_info demo_domain_proc_info;

为了和/proc封装器交互,插件必须再定义一个回调函数,并且为domain_proc_info结构体赋值给一个指针:

static long demo_get_domain_proc_info(struct domain_proc_info **ret) {
    *ret = &demo_domain_proc_info;
    return 0;
}

随后我们会更新sched_plugin结构体实例来把demo_get_domain_proc_info回调函数加入进去,但是我们要先添加初始化demo_domain_proc_info的代码。当插件被激活时,当前的拓扑结构必须存到demo_domain_proc_info中。为了简单,在一个demo这种简单分区插件里,每个处理器都会形成自己的调度域。下面的初始化代码会迭代所有的CPU,并且为每个CPU对应的调度域创建入口,在demo_activate_plugin()函数之前添加如下代码:

static void demo_setup_domain_proc(void) {
    int i, cpu;
    int num_rt_cpus = num_online_cpus();

    struct cd_mapping *cpu_map, *domain_map;

    memset(&demo_domain_proc_info, 0, sizeof(demo_domain_proc_info));
    init_domain_proc_info(&demo_domain_proc_info, num_rt_cpus, num_rt_cpus);
    demo_domain_proc_info.num_cpus = num_rt_cpus;
    demo_domain_proc_info.num_domains = num_rt_cpus;

    i = 0;
    for_each_online_cpu(cpu) {
        cpu_map = &demo_domain_proc_info.cpu_to_domains[i];
        domain_map = &demo_domain_proc_info.domain_to_cpus[i];

        cpu_map->id = cpu;
        domain_map->id = i;
        cpumask_set_cpu(i, cpu_map->mask);
        cpumask_set_cpu(cpu, domain_map->mask);
        ++i;
    }
}

然后我们在demo_activate_plugin()函数对demo_setup_domain_proc()函数进行调用:

static long demo_activate_plugin(void) {
    int cpu;
    struct demo_cpu_state *state;

    for_each_online_cpu(cpu) {
        TRACE("Initializing CPU%d...\n", cpu);
        state = cpu_state_for(cpu);
        state->cpu = cpu;
        state->scheduled = NULL;
        edf_domain_init(&state->local_queues,
                        demo_check_for_preemption_on_release,
                        NULL);
    }
    demo_setup_domain_proc(); // 设置域信息
    return 0;
}

销毁/proc状态

litmusRT支持另一个回调函数,这个回调函数会在插件被卸载时被调用。我们使用这个回调函数来让litmus清除这个插件的/proc接口使用的所有状态,添加的回调函数如下:

static long demo_deactivate_plugin(void) {
    destroy_domain_proc_info(&demo_domain_proc_info);
    return 0;
}

回调注册

最后我们需要在demo_plugin结构体中注册demo_deactivate_plugin()和demo_setup_domain_proc()函数:

static struct sched_plugin demo_plugin = {
        .plugin_name            = "DEMO",
        .schedule               = demo_schedule,
        .task_wake_up           = demo_task_resume,
        .admit_task             = demo_admit_task,
        .task_new               = demo_task_new,
        .task_exit              = demo_task_exit,
        .get_domain_proc_info   = demo_get_domain_proc_info,
        .activate_plugin        = demo_activate_plugin,
        .deactivate_plugin      = demo_deactivate_plugin, // 注册销毁插件回调
        .complete_job           = complete_job,
};

测试

编译安装重启后,我们就可以对demo插件进行测试了

root@rtlab-computer:/home/rtlab# setsched demo
root@rtlab-computer:/home/rtlab# cat /dev/litmus/log > debug.txt &
[1] 2739
root@rtlab-computer:/home/rtlab# rtspin -v -p 1 10 100 0.5
rtspin/2885:1 @ 0.0050ms
    release:  711541545295ns (=711.54s)
    deadline: 711641545295ns (=711.64s)
    cur time: 711541554475ns (=711.54s)
    time until deadline: 99.99ms
    target exec. time:   9.50ms (95.00% of WCET)
    sleep_next_period() until 711641545295ns (=711.64s)
rtspin/2885:2 @ 100.2250ms
    release:  711641545295ns (=711.64s)
    deadline: 711741545295ns (=711.74s)
    cur time: 711641783836ns (=711.64s)
    time until deadline: 99.76ms
    target exec. time:   9.50ms (95.00% of WCET)
    sleep_next_period() until 711741545295ns (=711.74s)
rtspin/2885:3 @ 200.2192ms
    release:  711741545295ns (=711.74s)
    deadline: 711841545295ns (=711.84s)
    cur time: 711741778259ns (=711.74s)
    time until deadline: 99.77ms
    target exec. time:   9.50ms (95.00% of WCET)
    sleep_next_period() until 711841545295ns (=711.84s)
rtspin/2885:4 @ 300.2181ms
    release:  711841545295ns (=711.84s)
    deadline: 711941545295ns (=711.94s)
    cur time: 711841777192ns (=711.84s)
    time until deadline: 99.77ms
    target exec. time:   9.50ms (95.00% of WCET)
    sleep_next_period() until 711941545295ns (=711.94s)
rtspin/2885:5 @ 400.2252ms
    release:  711941545295ns (=711.94s)
    deadline: 712041545295ns (=712.04s)
    cur time: 711941784489ns (=711.94s)
    time until deadline: 99.76ms
    target exec. time:   9.50ms (95.00% of WCET)
    sleep_next_period() until 712041545295ns (=712.04s)

然后查看debug.txt中关于P1的内容,如下所示(部分):

可见,2885: 1、2、3、4、5都是2885:0的作业,而2885:0才是一个实时任务。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值