记录:解决定时任务无法及时处理任务

背景描述

        前段时间,有个场景,需要使用I2C设备,每隔10ms采集一次样本,样本在实时变化,因此样本必须非常准时才行,否则对数据分析的结果会造成巨大影响。最终时间误差控制在了1ms以内,但是每隔大概10s~20s中就会有一到两次时间误差超过3ms,负载高低相差不大,虽然通过了验收,但是心里总是一个疙瘩,久久不能释怀。

        现在终于解决了这个问题,使用iperf模拟网络负载满的情况和使用死循环和里面添加各种复杂计算模拟CPU用满的情况,单核1GHz Arm cortex-a7 CPU, 时间依然控制在了误差500us以内,感觉非常完美,因此长舒一口气,决定记录下这个美好时刻。先说一下早先的解决方案,再说完美解决方案暂且厚颜无耻的叫他完美解决方案吧🐶🐶🐶

早先的解决方案

        当时的解决方案是通过hrtimer(hrtimer需要有一个硬件timer定时器)+ workqueue, 并且将workqueue的优先级调高,如下代码片段

// 执行数据采集的任务,尽可能的耗时短,因为会操作i2c硬件设备,内部可能会有延时函数调用
static void sampling_work_function(struct work_struct *work)
{
    // ..  操作i2c设备读写寄存器,获取数据并转换为标准单位等简单的工作,不能时间过长,否则10ms万一不够用了,会影推迟下一次数据采集的
}

    sensor_mgr.i2c_wq = alloc_workqueue("SENSOR_NAME", WQ_HIGHPRI|WQ_MEM_RECLAIM|WQ_FREEZABLE| WQ_UNBOUND  | __WQ_ORDERED, 1);
    if (!sensor_mgr.i2c_wq)
    {
        rc = -ENOMEM;
        goto alloc_ordered_workqueue_err;
    }
    INIT_WORK(&sensor_mgr.i2c_work, sampling_work_function);
    init_waitqueue_head(&sensor_mgr.waitqueue);
// 9.5ms,当时设置这个数值的原因:
// 定时器一般不会提前时间到,所有我们设置提前一点,这样在误差大存在的时候,
// 希望可以减小一点,心里好受点 😂😂😂 
// 实际上这个主要原因不在这里,因为在这里的时候时间是准确的无误的,这里误差大概在几十个微秒,不到100微秒
const static ktime_t sampling_interval =  {9* NSEC_PER_MSEC + 500 * NSEC_PER_USEC };
static enum hrtimer_restart hrtimer_callback_function(struct hrtimer *hrtimer)
{
    sensor_mgr_t *mgr = container_of(hrtimer, sensor_mgr_t, hrtimer.timer);
    if (!atomic_read(&mgr->closed))
    {
        // 
        hrtimer_forward_now(hrtimer, sampling_interval);
        queue_work(mgr->i2c_wq, &mgr->i2c_work);
        // 注意这里返回值
        return HRTIMER_RESTART;
    }
    // 注意这里返回值,很重要,很重要,不分开处理
    // ,有时在hrtimer_cancel的时候会退不出来
    // 并且在调用hrtimer_cancel前要设置mgr->closed为真
    return HRTIMER_NORESTART;
}

完美解决方案

核心的改动就是使用内核线程替换掉工作队列,并设置调度策略SCHED_FIFO优先级最高实时优先级,如此这般,CPU在分配时间片时就会优先分给它,当被唤醒的时候就能及时响应,因此降低了时间误差。

注意:如果还是不行,说明有其他线程的优先级比他高或调度策略和优先级与他一样,那么就要找到是那个线程,看需求上哪个更紧急,哪个优先级高,根据需求调整优先级,不能自己随意调整


// 因为用户获取数据时,我将时间戳也带上了,时间精度精确到1ms,1.XXms就是1ms
// 用户空间打印看到,使用9.5ms的时候就没有看到过10ms的间隔,都是9ms,
// 于是将9.5ms改成了9.8ms,还是很少看到10ms的间隔,因此改成了9.85ms,
// 看到9ms和10ms大概各占一半了 计算时间间隔的代码写在了采样数据的位置处,这里没有写

const static ktime_t sampling_interval =
            {9 * NSEC_PER_MSEC + 850 * NSEC_PER_USEC };//9.85ms
static enum hrtimer_restart hrtimer_callback_function(struct hrtimer *hrtimer)
{
    sensor_mgr_t *mgr;
    hrtimer_forward_now(hrtimer, sampling_interval);
    mgr = container_of(hrtimer, sensor_mgr_t, hrtimer);
    if (!atomic_read(&mgr->closed))
    {
        wake_up_process(mgr->task);
        return HRTIMER_RESTART;
    }
    pr_info("%s %d\n", __FUNCTION__, __LINE__);
    return HRTIMER_NORESTART;
}
static int sampling_task_function(void* data)
{
    sensor_mgr_t *mgr = data;
    while(!kthread_should_stop())
    {
        set_current_state(TASK_UNINTERRUPTIBLE);
        schedule();
        if (!kthread_should_stop())
        {
            if (atomic_read(&mgr->need_calibrate))
            {
                // 有的传感器需要校准,在这里执行校准
                sensor_calibrate_task(mgr);
            }
            else
            {
                // 真正的采集
                sensor_sampling_task(mgr);
            }
        }
    }
    pr_info("%s %d\n", __FUNCTION__, __LINE__);
    return 0;
}

// 这里只是演示技术方案的代码,很多逻辑已经删除了
/*
 * Priority of a process goes from 0..MAX_PRIO-1, valid RT
 * priority is 0..MAX_RT_PRIO-1, and SCHED_NORMAL/SCHED_BATCH
 * tasks are in the range MAX_RT_PRIO..MAX_PRIO-1. Priority
 * values are inverted: lower p->prio value means higher priority.
 *
 * The MAX_USER_RT_PRIO value allows the actual maximum
 * RT priority to be separate from the value exported to
 * user-space.  This allows kernel threads to set their
 * priority to a value higher than any user task. Note:
 * MAX_RT_PRIO must not be smaller than MAX_USER_RT_PRIO.
 */
// 有效的实时优先级是 0 .. MAX_RT_PRIO-1
static const struct sched_param sch_param={.sched_priority=MAX_RT_PRIO-1};
// 用户打开驱动的设备节点时就要开始工作了,只有用户需要的时候才工作,否则没有意义
// 还费电
static int user_open(struct inode *inode, struct file * file)
{
    // ...... 其他的业务逻辑 ....
    mgr->task = kthread_run(sampling_task_function, mgr, "sampling-task");
    // 这里是重点,设置了内核线程的策略和优先级,
    // 使用了SCHED_FIFO策略(一种用于实时的策略),和最高优先级
    // 注意,我们的线程里不能干太多的活,否则会把CPU资源占用完,看门狗会重启系统
    // 调试时,我的板子上连看门狗都不能完全工作了,只是打印了一句看门狗干活了,
    // 系统就没反应了

    sched_setscheduler(mgr->task, SCHED_FIFO, &sch_param);
    // 这里选择了 CLOCK_BOOTTIME,因为这个数值不会减,不会暂停,
    // 对应了真实世界的真实时间,一直增加,不能暂停,我们测量的数据就是
    // 真实世界的真实的采样,数据分析也是这个
    hrtimer_init(&mgr->hrtimer, CLOCK_BOOTTIME, HRTIMER_MODE_REL);
    mgr->hrtimer.function = hrtimer_callback_function;
    hrtimer_start(&mgr->hrtimer, (ktime_t){40*NSEC_PER_MSEC}, HRTIMER_MODE_REL);
    // ...... 其他的业务逻辑 ....
    return 0;
}

 最后

        既然内核线程可以完成任务,那么用户空间创建的线程应该也是可以的。因为技术是类似的。用户空间使用pthread_create创建的线程也是可设置优先级和调度策略的,因此用户空间也是可以的。下面是DeepSeek给出的用户空间修改线程调度策略和优先级的demo

#include <stdio.h>
#include <stdlib.h>
#include <sched.h>
#include <time.h>
#include <unistd.h>

#define TIMER_INTERVAL_NS 10000000 // 10ms

int main() {
    struct timespec ts;
    ts.tv_sec = 0;
    ts.tv_nsec = TIMER_INTERVAL_NS;

    // 设置实时调度策略
    struct sched_param param;
    param.sched_priority = 50; // 设置优先级
    if (sched_setscheduler(0, SCHED_FIFO, &param) == -1) {
        perror("sched_setscheduler");
        exit(EXIT_FAILURE);
    }

    while (1) {
        // 采集数据
        printf("Collecting data...\n");
        usleep(4000); // 模拟4ms的数据采集时间

        // 高精度睡眠
        if (nanosleep(&ts, NULL) == -1) {
            perror("nanosleep");
            break;
        }
    }

    return 0;
}

杂项

     在思考解决方案的时候想到并实验了以下解决方案,经验证没能解决问题

  1. tasklet, 主要用于中断的下半部,不能有睡眠,要尽快完成,我们要操作i2c总线上的硬件,i2c总线即有使用i2c控制器实现的,有GPIO模拟实现的,GPIO模拟的会有延时操作才能完成

感谢

感谢百问网韦东山老师的免费视频给与的帮助,感谢DeepSeek给与的帮助

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值