摘要

在嵌入式系统开发中,定时任务是确保系统按预定计划正确执行功能的关键。通过结合 crontab 表达式和 C 语言,可以设计出精准且灵活的定时任务系统。本博客详细描述了如何在嵌入式开发中,使用 crontab 表达式来实现基于绝对时间的定时任务。内容包括架构设计、技术实现、实施过程及挑战与解决方案。

引言

嵌入式系统广泛应用于各类智能设备中,例如家庭自动化、工业控制和医疗设备。定时任务在这些系统中至关重要,例如按时执行数据采集、定期发送报告或在特定时间内激活某项功能。crontab 表达式是 UNIX/Linux 系统中常用的定时任务表达方式,它能精确地描述何时执行任务。本项目旨在将 crontab 表达式的灵活性引入到嵌入式系统定时任务中。

项目背景与目标

背景

传统的嵌入式系统中定时任务通常依赖于简单的定时器或 RTC(实时时钟)。这些方案通常只适用于相对简单的定时需求,难以满足复杂的时间控制需求。crontab 表达式能够灵活定义任务的执行时间,适用于更复杂的场景。

目标

  1. 设计并实现一个以 C 语言编写的定时任务管理系统,支持 crontab 表达式。
  2. 该系统能够在嵌入式设备上高效地运行,并精确地在指定时间点执行任务。
  3. 提供用户友好的接口,便于配置和管理定时任务。

架构设计

系统架构

系统采用模块化设计,主要模块包括:

  1. 时间管理模块:负责解析 crontab 表达式,并与当前系统时间进行匹配。
  2. 任务调度模块:根据时间管理模块的匹配结果,调度并执行预定任务。
  3. 配置管理模块:提供接口,让用户能够添加、移除或修改定时任务。

数据结构

Task 结构体定义如下:

typedef struct {
    char* cron_expression;
    void (*task_function)(void);
    int last_sec;
    int last_min;
    int last_hour;
    int last_mday;
    int last_mon;
    int last_year;
} Task;

Task 结构体包含 crontab 表达式和对应的任务函数指针以及触发时间信息。

技术实现

什么是cron表达式?

cron的表达式是字符串,实际上是由七子表达式,描述个别细节的时间表。这些子表达式是分开的空白,代表:

1. Seconds
2. Minutes
3. Hours
4. Day-of-Month
5. Month
6. Day-of-Week
7. Year (可选字段)

比如:0 0 12 ? \* WED表示每星期三下午12:00,0 \* \* \* \* \? \*表示每分钟的0秒。如下图所示,为cron示例和模拟运行的时间演示。
cron示例

cron常用表达式

如下图所示,是常用的Cron表达式:
cron常用表达式

crontab 表达式解析

实现 crontab 表达式的解析需要考虑分钟、小时、日期、月份及星期的转换和比较。以下是一个基本解析函数的示例:

// 用于判断一个字段是否匹配,包括通配符 '*' 和 '?'
int field_matches(const char* field, int value) {
    // 如果是通配符 '*' 或者 '?' 则匹配
    if (strcmp(field, "*") == 0 || strcmp(field, "?") == 0) {
        return 1;
    }
    // 否则转换成整数并匹配
    int field_value = atoi(field);
    return field_value == value;
}

// 将表达式分割为字段
int split_crontab_expression(const char* cron_exp, char fields[8][10]) {
    int i = 0;
    const char* start = cron_exp;
    const char* end = start;

    while (*end != '\0' && i < 8) {
        if (*end == ' ') {
            strncpy(fields[i], start, end - start);
            fields[i][end - start] = '\0';
            start = end + 1;
            i++;
        }
        end++;
    }

    if (i < 8 && start < end) {
        strncpy(fields[i], start, end - start);
        fields[i][end - start] = '\0';
        i++;
    }

    return i;
}

// 解析 crontab 表达式并匹配当前时间
int parse_crontab_expression(const char* cron_exp, struct tm* tm) {
    char fields[8][10];
    int field_count = split_crontab_expression(cron_exp, fields);

    if (field_count < 6 || field_count > 7) {
        fprintf(stderr, "Invalid crontab expression.\n");
        return 0;
    }

    int matches = field_matches(fields[0], tm->tm_sec) &&
                  field_matches(fields[1], tm->tm_min) &&
                  field_matches(fields[2], tm->tm_hour) &&
                  field_matches(fields[3], tm->tm_mday) &&
                  field_matches(fields[4], tm->tm_mon + 1) && // tm_mon 是从 0 开始计
                  field_matches(fields[5], tm->tm_wday);     // tm_wday 是从 0 (Sunday) 开始计

    // 如果表达式包含年字段,进行进一步比较
    if (field_count == 7) {
        matches = matches && field_matches(fields[6], tm->tm_year + 1900); // tm_year 是从 1900 开始计
    }

    return matches;
}

实施过程

  1. 项目初始化:设定目标,定义项目范围,选择合适的开发环境和工具。
  2. 系统设计:设计整体架构和模块,定义数据结构和接口。
  3. 代码实现:逐个实现并测试各个模块,包括 crontab 表达式解析、任务调度及用户接口。
  4. 集成测试:将各模块集成进行全局测试,确保系统按预期运行。
  5. 优化与调试:查找并解决问题,优化性能和资源使用。

挑战与解决方案

挑战

  1. 解析复杂的 crontab 表达式crontab 表达式包含多种格式,解析并不简单。本文中,我们把cron表达式限定在如下格式:秒 分钟 小时 日 月 星期 年,年份可选,且通配符仅支持*(表示匹配任意值)和?(表示忽略该字段)。
  2. 资源受限的嵌入式环境:嵌入式系统通常资源有限,需要高效利用。

解决方案

  1. 优化解析算法:采用高效的数据结构和算法,确保快速解析和时间匹配。
  2. 精简代码:剔除冗余代码,尽量减少内存和 CPU 占用,提高系统稳定性。

完整可运行测试的代码

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

typedef struct {
    char* cron_expression;
    void (*task_function)(void);
    int last_sec;
    int last_min;
    int last_hour;
    int last_mday;
    int last_mon;
    int last_year;
} Task;

// 用于判断一个字段是否匹配,包括通配符 '*' 和 '?'
int field_matches(const char* field, int value) {
    // 如果是通配符 '*' 或者 '?' 则匹配
    if (strcmp(field, "*") == 0 || strcmp(field, "?") == 0) {
        return 1;
    }
    // 否则转换成整数并匹配
    int field_value = atoi(field);
    return field_value == value;
}

// 将表达式分割为字段
int split_crontab_expression(const char* cron_exp, char fields[8][10]) {
    int i = 0;
    const char* start = cron_exp;
    const char* end = start;

    while (*end != '\0' && i < 8) {
        if (*end == ' ') {
            strncpy(fields[i], start, end - start);
            fields[i][end - start] = '\0';
            start = end + 1;
            i++;
        }
        end++;
    }

    if (i < 8 && start < end) {
        strncpy(fields[i], start, end - start);
        fields[i][end - start] = '\0';
        i++;
    }

    return i;
}

// 解析 crontab 表达式并匹配当前时间
int parse_crontab_expression(const char* cron_exp, struct tm* tm) {
    char fields[8][10];
    int field_count = split_crontab_expression(cron_exp, fields);

    if (field_count < 6 || field_count > 7) {
        fprintf(stderr, "Invalid crontab expression.\n");
        return 0;
    }

    int matches = field_matches(fields[0], tm->tm_sec) &&
                  field_matches(fields[1], tm->tm_min) &&
                  field_matches(fields[2], tm->tm_hour) &&
                  field_matches(fields[3], tm->tm_mday) &&
                  field_matches(fields[4], tm->tm_mon + 1) && // tm_mon 是从 0 开始计
                  field_matches(fields[5], tm->tm_wday);     // tm_wday 是从 0 (Sunday) 开始计

    // 如果表达式包含年字段,进行进一步比较
    if (field_count == 7) {
        matches = matches && field_matches(fields[6], tm->tm_year + 1900); // tm_year 是从 1900 开始计
    }

    return matches;
}

// 判断是否是新的一轮循环,可以触发任务
int should_trigger_task(Task* task, struct tm* tm) {
    return tm->tm_sec != task->last_sec ||
           tm->tm_min != task->last_min ||
           tm->tm_hour != task->last_hour ||
           tm->tm_mday != task->last_mday ||
           tm->tm_mon != task->last_mon ||
           tm->tm_year != task->last_year;
}

// 更新任务的最近触发时间
void update_last_triggered(Task* task, struct tm* tm) {
    task->last_sec = tm->tm_sec;
    task->last_min = tm->tm_min;
    task->last_hour = tm->tm_hour;
    task->last_mday = tm->tm_mday;
    task->last_mon = tm->tm_mon;
    task->last_year = tm->tm_year;
}

// 示例定时任务函数,打印当前时间
void example_task(void) {
    time_t now = time(NULL);
    struct tm* current_time = localtime(&now);
    printf("Task executed at: %02d:%02d:%02d\n",
           current_time->tm_hour, current_time->tm_min, current_time->tm_sec);
}

// 示例主函数
int main(int argc, char *argv[])
{
    const char* cron_exp = "0 * * * * ? *"; // 每分钟的第0秒执行一次
    Task tasks[1];
    tasks[0].cron_expression = strdup(cron_exp);
    tasks[0].task_function = example_task;
    tasks[0].last_sec = -1;   // 初始化为-1,以确保首次能够触发任务
    tasks[0].last_min = -1;
    tasks[0].last_hour = -1;
    tasks[0].last_mday = -1;
    tasks[0].last_mon = -1;
    tasks[0].last_year = -1;
    int task_count = 1;

    while (1) {
        time_t now = time(NULL);
        struct tm* current_time = localtime(&now);

        for (int i = 0; i < task_count; i++) {
            // 解析表达式,如果当前时间匹配且该任务未在当前循环时间内触发,则执行任务
            if (parse_crontab_expression(tasks[i].cron_expression, current_time) &&
                should_trigger_task(&tasks[i], current_time)) {

                tasks[i].task_function();
                update_last_triggered(&tasks[i], current_time); // 更新最后触发时间
            }
        }
        usleep(100 * 1000);  // 每100ms检查一次
    }

    return 0;
}

执行结果:
如下图所示,为上述测试代码实际运行的结果,可以看到每分钟的第0秒都能正常触发,测试通过。
cron测试程序执行结果

结论

通过结合 crontab 表达式和 C 语言编程,我们在嵌入式系统中实现了灵活且准确的定时任务调度系统。该系统不仅支持复杂多变的定时任务需求,还能在资源受限的嵌入式环境中高效运行。在项目实施过程中,我们通过优化解析算法和精简代码来克服了一些挑战。未来,我们可以进一步扩展该系统,增加对更多定时任务格式的支持,提升用户的配置和管理体验。