Skip to content

Commit a4db5bb

Browse files
authored
docs: 调整文档表达,改为平实技术风格 (#13)
统一调整设计文档与长尾文档的措辞:精简段末的总结性铺陈、 改写对仗式表述、还原抽象说法为直白技术描述,保留全部技术 事实、API/类型名与代码示例。中英文同步处理。 重点调整 topic_design、linux_shared_topic_design、concept 三篇设计文档,其余文档仅做局部措辞优化。
1 parent 9afda4d commit a4db5bb

53 files changed

Lines changed: 185 additions & 354 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/adv_coding/core/rw_semantics.md

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ sidebar_position: 1
88

99
基础 API 见 [IO 读写抽象](/docs/basic_coding/core/core-rw)[Operation 操作模型](/docs/basic_coding/core/core-op)。下文直接讨论这套 I/O 完成模型为什么这样组织。
1010

11-
`LibXR` 当前这套 I/O 完成模型可以看成三层:`Operation` 描述这次操作完成后该怎样反馈,`ReadPort / WritePort` 负责队列、busy 状态和完成交接,具体驱动只负责把硬件推进到“接受请求”或“完成请求”这两个边界。真正复杂的状态机留在 `Port`,而不是绑在 `Operation`;这不是偶然,而是设计本意
11+
`LibXR` 当前这套 I/O 完成模型可以看成三层:`Operation` 描述这次操作完成后该怎样反馈,`ReadPort / WritePort` 负责队列、busy 状态和完成交接,具体驱动只负责把硬件推进到“接受请求”或“完成请求”这两个边界。真正复杂的状态机留在 `Port`,而不是绑在 `Operation` 上。
1212

1313
`Operation` 故意被做成体积很小、可平凡拷贝的对象,只携带完成方式本身:`CALLBACK``BLOCK``POLLING``NONE`,外加对应的小型数据载体。这样做的目的,就是避免把复杂生命周期、waiter 归属、timeout 后交接之类的状态机塞进 `Operation` 本体。否则它一旦变成重对象,不仅复制和传递成本会上升,驱动层和端口层的边界也会开始混乱。
1414

@@ -26,9 +26,7 @@ sidebar_position: 1
2626
| `BLOCK_DETACHED` | timeout 或 reset 已把 waiter 分离,完成侧必须静默 |
2727
| `EVENT` | 数据先到、waiter 还没挂起;下一次调用必须先重查队列 |
2828

29-
这里最容易被误解的是 `EVENT`
30-
31-
它不是“读完成”,而是:
29+
这里最容易被误解的是 `EVENT`。它表示的不是“读完成”,而是:
3230

3331
- 数据先进入了软件队列
3432
- 但当时还没有可认领的挂起读请求
@@ -51,12 +49,7 @@ sidebar_position: 1
5149
| `BLOCK_CLAIMED` | 最终唤醒已经归当前 waiter 所有 |
5250
| `BLOCK_DETACHED` | timeout/reset 已把 waiter 分离,完成侧不能再 post |
5351

54-
这里的关键点是:
55-
56-
- 多线程并发写安全,并不是“无锁队列自己解决了一切”
57-
- 真正的外层安全边界是 `busy_` 这道原子门
58-
59-
也就是说,多个线程不会同时直接冲进驱动 `WriteFun()` 抢底层硬件;只有拿到 `LOCKED` 的那一条路径,才拥有这次提交的队列修改权和 kickoff 权。
52+
这里的关键点是:多线程并发写安全并不靠无锁队列本身,真正的外层安全边界是 `busy_` 这道原子门。多个线程不会同时直接冲进驱动 `WriteFun()` 抢底层硬件;只有拿到 `LOCKED` 的那一条路径,才拥有这次提交的队列修改权和 kickoff 权。
6053

6154
---
6255

@@ -74,7 +67,7 @@ sidebar_position: 1
7467
1. 如果驱动声称自己没进入 `PENDING`,端口不会再替它维护后续完成语义
7568
2. 如果驱动在返回非 `PENDING` 后,底层其实还在后台继续跑,就会出现语义错位
7669

77-
因此驱动设计里必须非常明确:什么时刻算“已经交给硬件”,什么时刻又只是暂时 busy、还没真正接单。`PENDING` 和 non-`PENDING` 的边界一旦划错,就会出现很难看的语义错位:端口以为这次调用已经终结,但底层还在后台继续推进,结果 `BUSY / TIMEOUT / late completion` 全部缠在一起。很多看上去像队列 bug 的问题,本质上都是这里没有钉牢
70+
因此驱动设计里必须非常明确:什么时刻算“已经交给硬件”,什么时刻又只是暂时 busy、还没真正接单。`PENDING` 和 non-`PENDING` 的边界一旦划错,端口会以为这次调用已经终结,但底层还在后台继续推进,结果 `BUSY / TIMEOUT / late completion` 全部缠在一起。很多看上去像队列 bug 的问题,其实都出在这里没有钉牢
7871

7972
## 4. `BLOCK` timeout 不是取消
8073

@@ -86,7 +79,7 @@ sidebar_position: 1
8679

8780
它不是绝对 deadline,也不自动意味着底层取消。
8881

89-
因此 `BLOCK timeout` 的真实含义是:它只限制这次同步等待窗口,并不保证已经被底层接受的操作会被撤销。timeout 之后要做的关键工作,不是“把硬件立刻停掉”,而是把完成归属处理对。只要底层已经启动,迟到完成就仍然可能发生;如果这时候还允许它继续 `post` 旧的 semaphore,就会出现重复唤醒。`BLOCK_DETACHED` 一类状态存在的意义,就是告诉完成路径:这次 waiter 已经不属于原调用者了,后续只能静默收尾,不能再把唤醒投给它。
82+
因此 `BLOCK timeout` 的真实含义是:它只限制这次同步等待窗口,并不保证已经被底层接受的操作会被撤销。timeout 之后要做的关键工作,不是把硬件立刻停掉,而是把完成归属处理对。只要底层已经启动,迟到完成就仍然可能发生;如果这时候还允许它继续 `post` 旧的 semaphore,就会出现重复唤醒。`BLOCK_DETACHED` 一类状态存在的意义,就是告诉完成路径:这次 waiter 已经不属于原调用者了,后续只能静默收尾,不能再把唤醒投给它。
9083

9184
## 5. `Reset()` 为什么也走 detach 模型
9285

@@ -104,10 +97,6 @@ sidebar_position: 1
10497
- 底层旧完成稍后到来
10598
- 老完成又把新的调用错误唤醒
10699

107-
---
108-
109-
所以 `Reset()` 和 timeout 在这里其实是同一类问题:都要先分离当前 waiter,再等旧交接彻底排空,而不是假装世界已经恢复干净。
110-
111100
## 6. `AsyncBlockWait` 的位置
112101

113102
`AsyncBlockWait` 不是给 `ReadPort / WritePort` 自己用的状态机替代品,它更像是:

docs/adv_coding/driver/block_timeout_semantics.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ sidebar_position: 3
8787
- 但最终仍要继续等那次已经归属当前 waiter 的完成
8888
- 返回的是最终 `block_result_`,不是 `TIMEOUT`
8989

90-
所以 `BLOCK timeout` 不是简单的“超时就一定失败”,而是要看这次完成最后归谁所有
90+
所以 `BLOCK timeout` 的结果要看这次完成最后归谁所有,超时返回并不一定等于失败
9191

9292
---
9393

docs/adv_coding/driver/dbf.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ sidebar_position: 1
88

99
基础 API 说明见 [DoubleBuffer(双缓冲区)](/docs/basic_coding/structure/double_buffer)
1010

11-
这里讨论的不是数据结构接口,而是它在驱动里的作用。双缓冲的价值不在于“多开一块内存”,而在于把硬件传输、下一块数据准备和上层提交这三件事错开只要当前块还在总线上,驱动就可以准备下一块,完成中断到来时只做切换和续传。这样得到的不是抽象上的优雅,而是更短的空窗和更稳定的时序
11+
这里讨论的不是数据结构接口,而是它在驱动里的作用。双缓冲的价值在于把硬件传输、下一块数据准备和上层提交这三件事错开只要当前块还在总线上,驱动就可以准备下一块,完成中断到来时只做切换和续传,从而缩短空窗、稳定时序
1212

1313
---
1414

docs/adv_coding/driver/uart_driver.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ UART 驱动对上统一暴露 `SetConfig(...)`、`Write(...)` 和 `Read(...)`。
1818

1919
`STM32UART``CH32UART` 是最典型的 MCU 实现。两者的共同点很明确:接收侧使用常驻 DMA,发送侧使用双缓冲,用户侧读写统一经过 `ReadPort / WritePort`,完成通知由 DMA 完成中断或空闲事件推进。
2020

21-
接收侧的关键不在于“开了 DMA”,而在于 DMA 一直保持活跃。ISR 只需要根据当前写指针和上次位置算出新增区间,把这段数据推进软件队列,再调用 `ProcessPendingReads(true)`这样做的好处是,接收路径没有“停下来重新 arm”这一拍,软件只负责追赶硬件写指针。
21+
接收侧的关键在于 DMA 一直保持活跃。ISR 只需要根据当前写指针和上次位置算出新增区间,把这段数据推进软件队列,再调用 `ProcessPendingReads(true)`这样接收路径没有“停下来重新 arm”这一拍,软件只负责追赶硬件写指针。
2222

23-
发送侧则是另一套节奏。`STM32UART``CH32UART` 都不会在每次 `Write(...)` 时简单地直接起一次 DMA,而是先看当前 active/pending 哪块缓冲可用:DMA 空闲就直接写 active 区并启动;DMA 正忙就把下一笔数据写进 pending 区,等发送完成中断切过去。真正关键的不是双缓冲本身,而是完成中断里的顺序:先续上传输,再更新前一笔操作状态。只要顺序反过来,高频路径里的空窗就会立刻变长。
23+
发送侧则是另一套节奏。`STM32UART``CH32UART` 都不会在每次 `Write(...)` 时简单地直接起一次 DMA,而是先看当前 active/pending 哪块缓冲可用:DMA 空闲就直接写 active 区并启动;DMA 正忙就把下一笔数据写进 pending 区,等发送完成中断切过去。完成中断里的顺序是关键:先续上传输,再更新前一笔操作状态。只要顺序反过来,高频路径里的空窗就会立刻变长。
2424

2525
## ESP32 路径
2626

@@ -34,4 +34,4 @@ UART 驱动对上统一暴露 `SetConfig(...)`、`Write(...)` 和 `Read(...)`。
3434

3535
## 这套设计在意什么
3636

37-
把不同平台的实现摊开来看,重点其实一直没变接收侧要尽量做到“硬件持续喂数据,软件按需取”;发送侧要尽量做到“当前块还在发,下一块已经准备好”;完成通知要尽量短路径地推进下一次传输。满足这三点,驱动就不会太差;做不到,再漂亮的接口也救不了热路径
37+
把不同平台的实现摊开来看,重点其实一直没变接收侧要尽量做到“硬件持续喂数据,软件按需取”;发送侧要尽量做到“当前块还在发,下一块已经准备好”;完成通知要尽量短路径地推进下一次传输。这三点决定了驱动在热路径上的实际表现

docs/adv_coding/middleware/linux_shared_topic_design.md

Lines changed: 41 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,28 @@ sidebar_position: 2
66

77
# LinuxSharedTopic 设计
88

9-
基础用法见基础消息系统里的“共享内存 Topic(Linux)”页面。下面直接讨论它与普通 `Topic` 的边界,以及现在这套实现的取舍
9+
基础用法见基础消息系统里的“共享内存 Topic(Linux)”页面。本文说明它与普通 `Topic` 的分工,以及当前实现的取舍
1010

11-
## 1. 为什么不是直接改 `Topic`
11+
## 1. 为什么不直接改 `Topic`
1212

13-
`LinuxSharedTopic<T>` 解决的是 Linux / Webots 主机进程间通信、大 payload 共享、零拷贝读取以及多订阅者队列策略;原始 `Topic` 更像是进程内发布订阅,语义偏 MCU 和轻量系统,用精确类型分发、回调、同步/异步订阅者去组织数据流。这两条路径的约束根本不同,所以共享内存语义没有继续塞回 `Topic` 本体,而是单独做成 `LibXR::LinuxSharedTopic<T>`。这样 `Topic` 仍然保持轻量,Linux 主机 IPC 也可以沿着共享内存模型单独演化
13+
`LinuxSharedTopic<T>` 面向 Linux / Webots 主机的进程间通信:大 payload 共享、零拷贝读取、多订阅者队列策略。`Topic` 面向进程内发布订阅,偏 MCU 和轻量系统,用精确类型分发、回调、同步/异步订阅者组织数据流。两者约束不同,所以共享内存语义没有塞回 `Topic`,而是单独实现为 `LibXR::LinuxSharedTopic<T>`,让 `Topic` 保持轻量,主机 IPC 也能沿共享内存模型独立演化
1414

1515
## 2. 数据面和控制面分离
1616

17-
`LinuxSharedTopic<T>` 的核心结构不是一条简单队列,而是两层
17+
`LinuxSharedTopic<T>` 的核心结构分两层,而不是一条简单队列
1818

1919
1. payload slot
20-
- 真实数据驻留在共享内存槽位里
20+
- 真实数据存放在共享内存槽位里
2121
2. descriptor queue
2222
- 发布时只把“哪个 slot 可读”投递给订阅者
2323

24-
这样做的直接结果是
24+
这样
2525

2626
- payload 本体不需要在 publisher 和 subscriber 之间重复拷贝
27-
- 每个订阅者只需要消费描述符
28-
- 同一个 slot 可以被多个订阅者同时持有,直到最后一个释放
27+
- 每个订阅者只需消费描述符
28+
- 同一个 slot 可被多个订阅者同时持有,直到最后一个释放
2929

30-
这也是它能做到零拷贝的前提
30+
这是它实现零拷贝的前提
3131

3232
---
3333

@@ -39,171 +39,102 @@ sidebar_position: 2
3939
- publish / consume 侧尽量只做原子状态推进
4040
- 等待路径再用 futex 睡眠
4141

42-
这意味着它追求的不是“完全无等待”,而是尽量把 mutex 从 publish/consume 热路径里拿掉,把真正的等待压缩到 futex 睡眠那一层。
42+
它的目标不是完全无等待,而是把 mutex 从 publish/consume 热路径里拿掉,把真正的等待收拢到 futex 睡眠那一层。
4343

4444
## 4. 为什么用 refcount reclaim,而不是覆盖写
4545

46-
共享内存队列最危险的问题之一是:
46+
共享内存队列的一个危险场景是:publisher 想继续写,但某个 subscriber 还没读完旧数据。
4747

48-
- publisher 想继续写
49-
- 但某个 subscriber 还没把旧数据读完
50-
51-
这里选的是:
52-
53-
- refcounted slot reclamation
54-
- slot 用尽时对 publisher 施加 backpressure
55-
56-
而不是:
57-
58-
- overwrite-in-use
59-
60-
这样选的代价是:
61-
62-
- 高压下 publisher 可能因为 slot 耗尽而失败
63-
64-
好处是:
65-
66-
- 不会在 subscriber 仍持有数据时把 payload 直接覆盖掉
67-
68-
这是一条很明确的安全优先边界。
48+
这里用 refcounted slot reclamation,slot 用尽时对 publisher 施加 backpressure,而不是 overwrite-in-use。代价是高压下 publisher 可能因 slot 耗尽而失败;好处是不会在 subscriber 仍持有数据时把 payload 覆盖掉。这是安全优先的取舍。
6949

7050
---
7151

72-
## 5. 三种订阅策略在取舍什么
52+
## 5. 三种订阅策略的取舍
7353

74-
这里的订阅模式有
54+
订阅模式有三种,各自的目标和代价不同
7555

7656
- `BROADCAST_FULL`
7757
- `BROADCAST_DROP_OLD`
7858
- `BALANCE_RR`
7959

80-
它们不是“功能不同而已”,而是在取舍不同目标。
81-
8260
### `BROADCAST_FULL`
8361

84-
目标:
85-
86-
- 保留所有投递内容
87-
88-
代价:
89-
90-
- 某个慢订阅者满队列时,会直接把 publish 成功率拉低
62+
保留所有投递内容。代价是某个慢订阅者满队列时,会拉低 publish 成功率。
9163

9264
### `BROADCAST_DROP_OLD`
9365

94-
目标:
95-
96-
- 尽量保住 publisher 吞吐
97-
- 让慢订阅者始终更偏向读到新数据
98-
99-
代价:
100-
101-
- 订阅者会丢旧样本
66+
优先保住 publisher 吞吐,让慢订阅者更偏向读到新数据。代价是订阅者会丢旧样本。
10267

10368
### `BALANCE_RR`
10469

105-
目标:
106-
107-
- 多个 worker 间均衡分发
108-
109-
代价:
110-
111-
- 同一条消息不会广播给每个 worker
112-
113-
所以它本质上不是广播模式,而是共享负载模式。
70+
在多个 worker 间均衡分发。同一条消息不会广播给每个 worker,所以它是共享负载模式,不是广播模式。
11471

11572
---
11673

11774
## 6. `FULL``DROP_OLD` 的实际差异
11875

119-
这不是纯概念取舍。在慢订阅者过载场景下,实际行为是
76+
在慢订阅者过载场景下,两者的实际行为是
12077

121-
- `DROP_OLD` 能保住 publisher throughput
122-
- 同时显著降低 delivered sample latency
123-
- `FULL` 会保住队列内容完整性
124-
- 但 publish 成功率会明显下降,延迟也会抬高
78+
- `DROP_OLD` 能保住 publisher throughput,同时明显降低 delivered sample latency
79+
- `FULL` 保住队列内容完整,但 publish 成功率明显下降,延迟也会抬高
12580

126-
也就是说:
127-
128-
- 如果你要“所有样本都不能丢”,选 `FULL`
129-
- 如果你要“尽量快地拿到新数据”,选 `DROP_OLD`
130-
131-
这和控制场景里 freshness-first 的取舍是一致的。
81+
所以:所有样本都不能丢就选 `FULL`,要尽快拿到新数据就选 `DROP_OLD`。这和控制场景里 freshness-first 的取舍一致。
13282

13383
---
13484

135-
## 7. `BALANCE_RR` 不是“随便轮询一下”
85+
## 7. `BALANCE_RR` 的语义
13686

137-
`BALANCE_RR` 不是在广播路径上随手加个游标,而是独立的 balanced subscriber group。
87+
`BALANCE_RR` 是独立的 balanced subscriber group,不是在广播路径上加个游标
13888

139-
其行为边界包括
89+
行为边界
14090

14191
- 一个 publish 最多只投给一个 balanced subscriber
142-
- full member 会被跳过,只要组内还有别的成员能接
143-
- 如果 balanced group 存在,但没有任何 member 能接,则整个 publish 失败
144-
145-
所以它的语义更接近:
146-
147-
- “一组 worker 共享消费同一 topic”
92+
- 只要组内还有别的成员能接,full member 会被跳过
93+
- balanced group 存在但没有任何 member 能接时,整个 publish 失败
14894

149-
而不是:
150-
151-
- “广播之后顺便轮流处理”
95+
它的语义是“一组 worker 共享消费同一 topic”,不是“广播之后顺便轮流处理”。
15296

15397
---
15498

15599
## 8. 为什么需要 stale subscriber / publisher takeover
156100

157-
共享内存 IPC 最容易留下的垃圾不是日志,而是
101+
共享内存 IPC 容易残留两类垃圾
158102

159103
- 死掉的 subscriber 还占着 slot
160104
- 死掉的 publisher 留下旧 segment
161105

162-
这套实现已经把这两件事都考虑进来了:
163-
164-
- dead subscriber recycle
165-
- stale publisher takeover
106+
当前实现对两者都做了处理:
166107

167-
subscriber 侧会跟踪 owner identity
168-
publisher 侧会在 create-side reopen 时尝试回收死进程遗留的 segment
108+
- dead subscriber recycle:subscriber 侧跟踪 owner identity
109+
- stale publisher takeover:publisher 侧在 create-side reopen 时回收死进程遗留的 segment
169110

170-
这一步如果没有,Linux IPC 系统跑久了之后一定会出现“逻辑上没人用了,但共享状态还卡着”的问题。
111+
没有这一步,Linux IPC 跑久了会出现“逻辑上没人用了,但共享状态还卡着”的问题。
171112

172113
---
173114

174-
## 9. `latency_avg` 为什么经常不值得直接看
175-
176-
关于性能解读,当前最重要的一条结论是:
177-
178-
- standard-case `latency_avg` 很容易受 scheduler 和启动 backlog 污染
179-
180-
也就是说,如果 publisher 一开始就灌数据,而 subscriber 尚未完全进入稳态等待,那么:
181-
182-
- `avg latency` 里会混入一段队列堆积时间
183-
184-
这不等于纯粹的单次投递延迟。
115+
## 9. 为什么 `latency_avg` 经常不值得直接看
185116

186-
因此更有意义的区分通常是:
117+
standard-case 的 `latency_avg` 容易受 scheduler 和启动 backlog 污染:如果 publisher 一开始就灌数据、subscriber 还没进入稳态等待,`avg latency` 里会混进一段队列堆积时间,不等于单次投递延迟。
187118

188-
- saturated-throughput queueing latency
189-
- single-outstanding one-way latency
119+
更有意义的区分是:
190120

191-
前者描述系统满载时的排队表现,后者才更接近“消息本身从 publish 到被 wait 成功拿到”的真实单次路径。
121+
- saturated-throughput queueing latency:系统满载时的排队表现
122+
- single-outstanding one-way latency:消息从 publish 到被 wait 成功拿到的单次路径
192123

193124
---
194125

195-
## 10. 适合它,不适合它
126+
## 10. 适用与不适用场景
196127

197-
适合 `LinuxSharedTopic<T>` 的场景
128+
适合:
198129

199130
- Linux 主机多进程共享大 payload
200131
- 订阅者策略明确,需要 `FULL / DROP_OLD / RR`
201132
- 希望避免用户态额外 payload 拷贝
202133

203-
不适合它的场景
134+
不适合
204135

205136
- MCU 侧 ISR 驱动路径
206137
- 进程内轻量 publish/subscribe
207138
- payload 不是 trivially copyable
208139

209-
从定位上看,它不是普通 `Topic` 的升级版,而是主机 IPC 的专门实现
140+
它是主机 IPC 的专门实现,不是普通 `Topic` 的升级版。

0 commit comments

Comments
 (0)