18 嵌入式操作系统 | uloop 定时器:周期任务与网络上报

嵌入式操作系统 | uloop 定时器:周期任务与网络上报

本课程开源地址(Gitee) :https://gitee.com/fujianxinxi/qianrushixitongyingyongkaifa.git

课件、示例代码与验收脚本都在该仓库,可直接 git clone 或下载 ZIP 使用。
模块五 · 2 学时 · 环境:VMware + Ubuntu 20.04

一句话概述:讲清 uloop 的定时器用法:uloop_timeout_set 排一次、回调里再排一次就是周期任务;用"1 秒心跳 + 3 秒采集并 UDP 上报"的程序把定时器和网络收发放进同一个事件循环,一步一步给出命令与运行现象,配套课堂练习与课后作业。

本节目标

  1. 会用 uloop_timeout_set() 排一个定时任务,并知道周期任务是怎么实现的
  2. 会写"定时采集 + UDP 上报"的程序,让它和其它任务共用一个事件循环
  3. 会读懂程序打印的时间节奏(心跳和采集为什么是 1:3)

一、讲解内容

1.1 知识讲解

uloop 定时器就一个函数。 uloop_timeout_set(&t, 毫秒):把结构体 t 排进定时队列,到点调用 t.cb。注意单位是毫秒:1000 = 1 秒。

周期任务 = 回调里再排一次。 uloop 的定时器默认只响一次,想让它每 3 秒响一次,就在回调末尾再 uloop_timeout_set(t, 3000)------这就是"周期任务"的实现方式(嵌入式里的心跳、周期采集、超时重试都这么做)。

为什么比 sleep 好。 定时器回调之间,事件循环还能照常处理网络数据------sleep(3) 会把整个程序卡住 3 秒,期间来什么包都处理不了。

一个循环里可以挂很多任务。 本课同时挂两个定时器(心跳 1 秒、采集 3 秒)和一个 UDP 套接字,它们互不干扰:谁到点就执行谁。

结束条件要写清楚。 本课采满 3 次就 uloop_end() 退出,方便课堂观察;真实项目里通常一直跑,靠 Ctrl+C 或 systemd 管理(课时 07 学过)。

1.2 操作步骤(一条命令一步,每步都有现象)

第 1 步:编译

shell 复制代码
cd ~/lab/课时18-uloop定时器
gcc -Wall -Wextra -o uloop_timer uloop_timer.c -lubox

现象:无输出即成功。

第 2 步:先启动 Python 接收端(终端 A,先开)

shell 复制代码
python3 udp_receiver.py 9999

现象:

text 复制代码
UDP 接收端已启动,监听 9999 端口,等 C 程序上报,Ctrl+C 退出

第 3 步:终端 B 运行 C 程序(后开)

shell 复制代码
./uloop_timer 127.0.0.1 9999

现象(重点看两个节奏:心跳每秒一次,采集每 3 秒一次,采集后立刻上报;程序大约 9 秒后自己结束):

text 复制代码
① uloop 初始化完成
② 上报目标:127.0.0.1:9999
③ 已挂两个定时器:心跳 1 秒 / 采集 3 秒
④ 进入 uloop_run()

   [心跳] 第 1 次:程序还活着(uloop 在跑)
   [心跳] 第 2 次:程序还活着(uloop 在跑)
★ 第 1 次采集:温度 25.0 C
   → 已通过 UDP 上报
   [心跳] 第 3 次:程序还活着(uloop 在跑)
   [心跳] 第 4 次:程序还活着(uloop 在跑)
   [心跳] 第 5 次:程序还活着(uloop 在跑)
★ 第 2 次采集:温度 25.5 C
   → 已通过 UDP 上报
   [心跳] 第 6 次:程序还活着(uloop 在跑)
   [心跳] 第 7 次:程序还活着(uloop 在跑)
   [心跳] 第 8 次:程序还活着(uloop 在跑)
★ 第 3 次采集:温度 26.0 C
   → 已通过 UDP 上报
采集 3 次完成,结束事件循环
   [心跳] 第 9 次:程序还活着(uloop 在跑)

⑤ uloop 结束:共心跳 9 次,采集 3 次

终端 A(Python 接收端)的现象------上报的数据真的到了对面:

text 复制代码
第 1 条上报(来自 127.0.0.1:33973):第 1 次采集:温度 25.0 C
第 2 条上报(来自 127.0.0.1:33973):第 2 次采集:温度 25.5 C
第 3 条上报(来自 127.0.0.1:33973):第 3 次采集:温度 26.0 C

现象解读 :① 心跳和采集各自按自己的周期执行,说明一个事件循环里可以挂多个定时任务 ;② 第 3 次采集后程序自己结束(我们写了 uloop_end());③ 采集数据通过 UDP 发出去了(Python 那边收到了 3 条)。

第 4 步:改节奏看变化(动手改一改)

把 on_collect 里的 uloop_timeout_set(t, 3000) 改成 1000,重新编译运行------心跳和采集都变成每秒一次;把 COLLECT_MAX 从 3 改成 5,就采 5 次再结束。

第 5 步:收工。 终端 A 按 Ctrl+C 停掉 Python 接收端。

1.3 常见错误与排查

现象 原因 解决
只会响一次然后程序就结束了 回调里没有再 uloop_timeout_set 在回调末尾重新排下一次,才是周期任务
周期明显不对(快 10 倍/慢 1000 倍) 单位搞错(毫秒 vs 秒) uloop 用毫秒:1 秒要写 1000
程序一直跑不完 忘了结束条件 加计数,够了调用 uloop_end()(或按 Ctrl+C)
Python 端收不到数据 端口不一致、或先关掉了接收端 两边端口一致;接收端先启动
运行时 libubox.so: cannot open shared object file 没执行 sudo ldconfig 回课时 15 重新装并 sudo ldconfig

二、学生练习

本节命令速查表:

命令 作用 示例
gcc ... -lubox 编译 gcc -Wall -Wextra -o uloop_timer uloop_timer.c -lubox
python3 udp_receiver.py 端口 启动接收端(先开) python3 udp_receiver.py 9999
./uloop_timer IP 端口 运行程序 ./uloop_timer 127.0.0.1 9999
uloop_timeout_set(&t, 毫秒) 排一个定时任务 uloop_timeout_set(&beat, 1000)
uloop_timeout_cancel(&t) 取消定时任务 uloop_timeout_cancel(&beat)
编号 任务 提示 预期结果
L18.1 按步骤跑通 先起接收端,再跑 C 程序 心跳 1 秒 1 次、采集 3 秒 1 次,Python 收到 3 条
L18.2 把采集周期改成 1 秒 改 uloop_timeout_set(t, 1000) 心跳和采集都是每秒一次
L18.3 改成采 5 次 改 COLLECT_MAX 打印到第 5 次采集才结束
L18.4 让心跳每 2 秒一次 改心跳那个 1000 为 2000 心跳节奏变慢,采集不受影响
L18.5 加一个"上报失败"提示 把接收端关掉再运行程序 sendto 依然成功(UDP 不保证送达),这说明 UDP 只管发出去
L18.6 用 date 观察真实时间 在心跳回调里打印 time(NULL) 两次心跳时间差约 1 秒

三、作业

  • 必做:完成 L18.1、L18.2,提交 C 程序输出截图与 Python 接收端截图,并回答:① 周期定时任务是怎么实现的?② uloop 定时器的单位是什么?③ 为什么定时器比 sleep() 好?
  • 选做:把"采集"改成读一个真实数据(比如 /proc/loadavg 里的负载值),再上报给 Python 端
  • 提交:截图 + 文字回答,命名 课时18-作业截图.png

四、本节小结与下节预告

本节掌握了 uloop 定时器:uloop_timeout_set 排任务、回调里再排就是周期任务,并做出了"定时采集 + UDP 上报"的完整小程序。下节换到配置:用 uci 读写 OpenWrt 风格的配置文件,一条命令一条命令地看结果。


附录:怎么运行 + 完整代码

一个课件就够用:先按附录 A 的顺序敲命令(服务端/对端先开,自己的程序后开),再按附录 B 抄代码。行尾请用 LF。

附录 A|怎么运行(注意先后顺序)

shell 复制代码
gcc -Wall -Wextra -o uloop_timer uloop_timer.c -lubox
python3 udp_receiver.py 9999                             # 终端A:先启动接收端
./uloop_timer 127.0.0.1 9999                             # 终端B:心跳 1 秒、采集 3 秒,采 3 次后自己结束

附录 B|文件 uloop_timer.c

c 复制代码
/*
 * uloop_timer.c ------ 课时 18:uloop 定时器(周期任务 + 网络上报)
 *
 * 编译:gcc -Wall -Wextra -o uloop_timer uloop_timer.c -lubox
 * 运行:./uloop_timer 127.0.0.1 9999
 *
 * 两个定时任务跑在同一个事件循环里:
 *   · 心跳定时器:每 1 秒打印一次 "程序还活着"
 *   · 采集定时器:每 3 秒采集一次数据,并通过 UDP 上报给 Python 工具
 * 采满 3 次后调用 uloop_end() 退出(课堂上不用手动 Ctrl+C,现象更清楚)。
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <libubox/uloop.h>

#define COLLECT_MAX 3            /* 采 3 次就结束,方便观察 */

static struct uloop_timeout beat;      /* 1 秒心跳 */
static struct uloop_timeout collect;   /* 3 秒采集 */
static struct sockaddr_in   server;
static int                  sock = -1;
static int                  beat_cnt = 0;
static int                  collect_cnt = 0;

/* 心跳:定时器回调里重新排下一次,就变成周期任务 */
static void on_beat(struct uloop_timeout *t)
{
    beat_cnt++;
    printf("   [心跳] 第 %d 次:程序还活着(uloop 在跑)\n", beat_cnt);
    uloop_timeout_set(t, 1000);
}

/* 采集:采一次、上报一次;采满 COLLECT_MAX 次就结束事件循环 */
static void on_collect(struct uloop_timeout *t)
{
    char msg[128];
    float temp = 25.0f + collect_cnt * 0.5f;    /* 假装采到的温度 */

    collect_cnt++;
    snprintf(msg, sizeof(msg), "第 %d 次采集:温度 %.1f C", collect_cnt, temp);
    printf("★ %s\n", msg);

    if (sendto(sock, msg, strlen(msg), 0,
               (struct sockaddr *)&server, sizeof(server)) < 0) {
        perror("sendto");
    } else {
        printf("   → 已通过 UDP 上报\n");
    }

    if (collect_cnt >= COLLECT_MAX) {
        printf("采集 %d 次完成,结束事件循环\n", COLLECT_MAX);
        uloop_end();
        return;
    }
    uloop_timeout_set(t, 3000);
}

int main(int argc, char *argv[])
{
    int port = 9999;

    if (argc >= 3) {
        port = atoi(argv[2]);
    }

    uloop_init();
    printf("① uloop 初始化完成\n");

    sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0) {
        perror("socket");
        return 1;
    }
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port   = htons((uint16_t)port);
    inet_pton(AF_INET, argc >= 2 ? argv[1] : "127.0.0.1", &server.sin_addr);
    printf("② 上报目标:%s:%d\n", argc >= 2 ? argv[1] : "127.0.0.1", port);

    beat.cb = on_beat;
    uloop_timeout_set(&beat, 1000);
    collect.cb = on_collect;
    uloop_timeout_set(&collect, 3000);
    printf("③ 已挂两个定时器:心跳 1 秒 / 采集 3 秒\n");

    printf("④ 进入 uloop_run()\n\n");
    uloop_run();

    close(sock);
    uloop_done();
    printf("\n⑤ uloop 结束:共心跳 %d 次,采集 %d 次\n", beat_cnt, collect_cnt);
    return 0;
}

附录 C|文件 udp_receiver.py

python 复制代码
#!/usr/bin/env python3
"""课时 18 测试工具:UDP 数据接收端(打印 C 程序上报的每条数据)

用法:python3 udp_receiver.py [端口]      # 端口默认 9999,Ctrl+C 退出
"""
import socket
import sys

port = int(sys.argv[1]) if len(sys.argv) > 1 else 9999

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("0.0.0.0", port))
print(f"UDP 接收端已启动,监听 {port} 端口,等 C 程序上报,Ctrl+C 退出")

count = 0
try:
    while True:
        data, addr = s.recvfrom(1024)
        count += 1
        print(f"第 {count} 条上报(来自 {addr[0]}:{addr[1]}):{data.decode('utf-8', 'replace')}")
except KeyboardInterrupt:
    print(f"\n接收端退出,共收到 {count} 条")
finally:
    s.close()
相关推荐
多多爱学习1 小时前
括号有几层?得看还有多少个左括号没闭合
c语言·c++·算法·面试
AI+程序员在路上2 小时前
AP6275S蓝牙双接口解析:HCI UART与PCM的分工与协同
linux·c语言·物联网
jingli92 小时前
本地向量库索引把 C 盘塞爆 48.6G:一次 LanceDB 膨胀排查与根治实录
c语言·开发语言
阿钱真强道3 小时前
21 嵌入式操作系统 | JSON 与 json-c:生成与解析
c语言·开发语言·json·json-c
心中有你02146 小时前
C语言埃氏筛法(素数筛选)
c语言·数据结构·算法
晊晌_h15 小时前
IMX6ULL裸机开发:定时器(GPT/EPIT)与UART串口
arm·定时器·arm裸机开发
晓蛋6 天前
c语言指的是什么意思
c语言·编译器·编程开发·集成开发环境·程序实例
傲世仙尊6 天前
目录即文件-Ext文件系统收尾篇
linux·c语言
牵猫散步的鱼儿6 天前
重载、重写(覆盖)、重定义区别
c语言