嵌入式操作系统 | uloop 定时器:周期任务与网络上报
本课程开源地址(Gitee) :https://gitee.com/fujianxinxi/qianrushixitongyingyongkaifa.git
课件、示例代码与验收脚本都在该仓库,可直接
git clone或下载 ZIP 使用。
模块五 · 2 学时 · 环境:VMware + Ubuntu 20.04
一句话概述:讲清 uloop 的定时器用法:uloop_timeout_set 排一次、回调里再排一次就是周期任务;用"1 秒心跳 + 3 秒采集并 UDP 上报"的程序把定时器和网络收发放进同一个事件循环,一步一步给出命令与运行现象,配套课堂练习与课后作业。
本节目标
- 会用
uloop_timeout_set()排一个定时任务,并知道周期任务是怎么实现的 - 会写"定时采集 + UDP 上报"的程序,让它和其它任务共用一个事件循环
- 会读懂程序打印的时间节奏(心跳和采集为什么是 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()