RISC-V Linux 嵌入式编程技术 · 第六章

本章实验 · 三个实验

实验一 race-demo + TSan · 实验二 成对快照加锁 · 实验三 三线程协同(主实验)

本章三个实验一览

第六章主实验要接 MQTT、风扇与三线程产品骨架。为了不一把抓,拆成三个递进实验:实验一、实验二纯软件、无接线、无 Broker,先看清「丢更新」和「成对快照不一致」;实验三再接到真硬件与 MQTT。

实验练什么器材源码目录
实验一
race-demo + TSan
无锁计数丢更新;TSan 指出抢同一变量的两行 无 code/race-demo/
实验二
成对快照 + 互斥锁
写/读 temp_a/temp_b;无锁必现 [RACE],加锁后 race_hits=0 无 code/snapshot-lock/
实验三
三线程协同
采集∥控制∥MQTT;把实验二的成对加锁接到产品路径;Ctrl+C 关风扇 风扇/继电器(SIMULATE 可先不接 DHT) code/tri-thread/
建议:先完成实验一、实验二(soft),再进实验三。race-demo / snapshot-lock 脚手架故意无锁,不要抄进 tri-thread 的验收路径;板上验收以 USE_LOCK 1 为准。

实验一 · race-demo + TSan

目的:用「很多线程同时给同一计数器加一」亲眼看到最终计数偏小,再用 TSan 指出具体是哪两行在抢。主机或板端均可;下面摘录来自板端。

$ cd chapters/ch06/code/race-demo
$ make && ./race-demo
expect=400000  got=215946  RACE(lost updates)

主机跑 TSan 若报 FATAL: ThreadSanitizer: unexpected memory mapping,用下面任一种方式规避 ASLR:sudo sysctl -w vm.mmap_rnd_bits=28,或直接用 setarch $(uname -m) -R ./race-demo(无需 root;板端可直接跑,不用处理)。

$ make tsan && ./race-demo
WARNING: ThreadSanitizer: data race
  Read of size 8 ... by thread T2:
    #0 bump_racy .../main.c:24
  Previous write of size 8 ... by thread T1:
    #0 bump_racy .../main.c:26
SUMMARY: ThreadSanitizer: data race .../main.c:24 in bump_racy

本实验成功判据:有「计数偏小」记录;有完整 TSan 报告;能指出抢变量的两行。

板上 race-demo:expect 与 got 不一致,标出 RACE
板端证据(SSH → race-demo):最终计数偏小并标出 RACE
板上 make tsan 后 race-demo 的 ThreadSanitizer 报告
TSan 证据:make tsan 后 ./race-demo 给出 WARNING: ThreadSanitizer: data race,指出 bump_racy 里读/写同一全局计数的两行

实验二 · 成对快照 + 互斥锁(snapshot-lock)

实验三卡人的往往是「同一次温度拆成 temp_a/temp_b」:无锁时控制线程读到半截快照就打 [RACE]。本实验把这对写入/读出剥成纯软件:写线程 pair_write,读线程 pair_read。源码 chapters/ch06/code/snapshot-lock/,只实现这两个函数(脚手架已故意留空窗)。

# 主机,或先 scp 到板子再进目录
$ cd chapters/ch06/code/snapshot-lock      # 板上:cd ~/snapshot-lock
$ make && ./build/snapshot-lock
# 未加锁 → 大量 [RACE],race_hits 很多(预期)
$ make sol && ./build/snapshot-lock-sol
# 参考:race_hits=0

打开 src/pair.c:用同一把 pthread_mutex 包住成对写与成对读;加锁后不要在中间 usleep。对照 pair-sol.c。

本实验成功判据:脚手架能打出 [RACE];make sol 后 race_hits=0;能口头说这套成对加锁平移到实验三就是 USE_LOCK 1。

板上 snapshot-lock:脚手架现 RACE,sol 后 race_hits=0
板端证据(SSH → snapshot-lock):无锁 [RACE];加锁版 race_hits=0
加锁版跑完打印 race_hits=0
加锁版快照:跑完打印 race_hits=0
无锁脚手架完整跑完打印最终 race_hits
无锁快照:脚手架完整跑完(不再截断输出),末行给出最终 race_hits

实验三 · 三线程协同(主实验)

打开 chapters/ch06/code/tri-thread/。Broker 用 BROKER_HOST(主机先 hostname -I)。脚手架默认 USE_LOCK 0:采集把同一次温度拆成 temp_a/temp_b 两步写并留空窗(与实验二同一招),控制线程读到不一致就打印 [RACE]。

  1. 先 USE_LOCK 0,跑 10–20 秒,应反复出现:
    [RACE] inconsistent snapshot temp_a=275 temp_b=270
    退出时 race_hits 远大于 0。
  2. 改为 USE_LOCK 1 重编再跑——改宏后必须 clean,否则目标文件不会重编:
    $ make clean && make USE_LOCK=1
    $ BROKER_HOST=<主机局域网IP> ./tri-thread
    无 [RACE],race_hits=0;风扇按阈值动作。
  3. 编译运行(板端原生或主机交叉):
    $ cd chapters/ch06/code/tri-thread
    $ make clean && make
    $ BROKER_HOST=<主机局域网IP> ./tri-thread
    # 或:make CROSS_COMPILE=riscv64-ruyisdk-linux-gnu- && scp …
  4. MQTT / 干净停:主机 mosquitto_pub … -t course/thermo/cmd -m 'status'(还有 fan on / fan off / fan auto、set high 30),板端见 [MQTT] cmd payload=...;Ctrl+C 后风扇关并 exited cleanly。强制开关不会被自动控制覆盖:下发 fan off 后即使温度高于上限,状态仍是 fan=off mode=force-off。

实机摘录(USE_LOCK=1):

[INFO] USE_LOCK=1 SIMULATE_SENSOR=1
[MQTT] subscribed course/thermo/cmd
[INFO] fan ON
[MQTT] cmd payload=set high 30 (len=11)
[INFO] race_hits=0 (USE_LOCK=1;期望 0→很多,1→0)
[INFO] exited cleanly

MQTT 命令与并发的实机转录(USE_LOCK=1,主机侧发命令)。注意 fan off 那次温度已高于上限,自动控制没有把它翻回 ON——读、判、写在同一把锁内完成;最后一条 fan on 长度是 11 而不是 6,那是带内嵌 NUL 的伪造载荷 fan on\0junk,被拒且不改状态:

[MQTT] cmd payload=fan on (len=6)
[INFO] fan ON
[MQTT] status temp=30.5 hum=55.0 fan=on mode=force-on high=28.0 low=26.0
[MQTT] cmd payload=fan off (len=7)
[INFO] fan OFF
[MQTT] status temp=30.5 hum=55.0 fan=off mode=force-off high=28.0 low=26.0
[MQTT] cmd payload=set high 30 (len=11)
[MQTT] status temp=28.5 hum=55.0 fan=off mode=force-off high=30.0 low=26.0
[MQTT] cmd payload=fan on (len=11)
[ERR] unknown command payload (len=11), ignored
[MQTT] status temp=26.5 hum=55.0 fan=off mode=force-off high=30.0 low=26.0
[MQTT] cmd payload=fan auto (len=8)
[MQTT] fan mode auto

命令按 payloadlen 精确匹配(与第五章同一个坑):载荷必须与命令等长且逐字节相同,不能先当 C 字符串再 strcmp——否则 fan on\0junk 会被截断成合法的 fan on。实测被拒的还有 status\0、set high 30\0x、fan autoo,板端一律打印 [ERR] unknown command payload,不改风扇、不发 status。

另外两处「不假装成功」:订阅失败会报错并按断线重连(不会只打一行 subscribed 就算连上);status 的发布失败会报 [ERR] status publish,不再打出假的成功行。Broker 连续不可达达上限后只收 MQTT 这一条腿,采集/控制线程继续跑本地演示(MQTT disabled; sense/control keep running),不再把整个程序带走。

板上 tri-thread:USE_LOCK 0 现 RACE,USE_LOCK 1 后 race_hits=0
板端证据(SSH → tri-thread):无锁出现 [RACE];加锁后 race_hits=0 并干净退出
USE_LOCK=0 时出现 RACE 的终端快照
无锁快照:反复 [RACE] inconsistent snapshot
USE_LOCK=1 时 race_hits=0 的终端快照
加锁快照:race_hits=0,Ctrl+C 干净停
实物对照(手机拍摄):主机发 fan on/off/auto 与 led on/off,画面里继电器吸合、风扇转停、LED 亮灭——三线程的控制线程驱动的就是这条信号链。同一段接线在综合项目页复用。

验收

实验项通过条件□
一丢更新race-demo 最终计数明显小于期望
一TSan报告已保存,能指出抢变量的两行
二无锁快照snapshot-lock 脚手架出现 [RACE] 且 race_hits>0
二加锁快照make sol 后 race_hits=0
三无锁USE_LOCK 0 出现 [RACE] 且 race_hits>0
三加锁USE_LOCK 1 无 [RACE],风扇阈值正常
三MQTT下行或上行至少一项可演示
三干净停Ctrl+C 后风扇关,进程退出
三实物命令与实物动作对得上:继电器吸合、风扇转停、LED 亮灭
完成标志:上表全部勾选。本骨架供综合项目复用。