背景描述
前段时间,有个场景,需要使用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, ¶m) == -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;
}
杂项
在思考解决方案的时候想到并实验了以下解决方案,经验证没能解决问题
- tasklet, 主要用于中断的下半部,不能有睡眠,要尽快完成,我们要操作i2c总线上的硬件,i2c总线即有使用i2c控制器实现的,有GPIO模拟实现的,GPIO模拟的会有延时操作才能完成
感谢
感谢百问网韦东山老师的免费视频给与的帮助,感谢DeepSeek给与的帮助