应用的图形与事件(1) — 点画星辰、直线,和第一次「阻塞等待」¶
这一章讲什么: 应用已经有了一扇窗,本章教它在窗里画画、等事件: 画点画矩形(一个伪随机星空应用)、拿计时器值测自己多慢、批量绘制时关掉自动重绘 (第 9、15 章的性能纪律走进系统调用)、画直线(斜率的两种情况)、关窗, 最后是最重要的一件:ReadEvent——应用阻塞等按键/定时器等事件,Ctrl+Q 优雅退出。 API 每多一件,应用的世界就宽一圈。
1. 先看现象:一个星空应用的生命周期¶
主线例子,stars 应用(本章各机制轮番上场):
开窗(上一章的 OpenWindow)→ exit() 能用了
(C 标准库 exit 内部要 atexit 机制,得给 Newlib 补一个 _exit() → 转调 SyscallExit)
WinFillRectangle 涂黑整个窗口 = 夜空
循环 100 次:伪随机坐标画 1×1 的矩形 = 星星
问题:每画一次系统自动重绘一次 —— 100 次全窗重绘,慢
SyscallGetCurrentTick:画之前计时、画完计时,差值换算成毫秒显示
「先测量,再优化」进了应用层
优化:所有绘制系统调用加 layer_flags 参数,第 0 位 = 1 → 不重绘;
100 颗星星画完,手动调一次 WinRedraw()
顺手:画辐射状直线(lines 应用,斜率 |m|>1 要换轴)、CloseWindow() 关窗
winhello 升级:ReadEvent 阻塞等事件,收到 Ctrl+Q(退出事件)才关窗退出
→ 窗口不再一闪而过;等待期间 CPU 占用永远不接近 100%
2. 顶层全景¶
22.1 exit():Newlib 的 exit 需要 _exit → 桥到 SyscallExit
22.2 画点 = 画 1×1 矩形;FillRectangle 一举多得
22.3 计时器值系统调用:diff / freq = 毫秒;测量批量绘制的耗时
22.4 重绘抑制:layer_flags 第 0 位;WinRedraw 手动重绘;兼容性靠「默认重绘」
22.5 直线:斜率 m = dy/dx;|m|>1(陡)时交换 x/y 轴再算;端点相同先按画点处理
22.6 CloseWindow = 删图层(RRemoveLayer)
22.7 ReadEvent:AppEvent 数组;无事件则任务休眠(第 14 章原语);
事件源 = 终端任务的消息队列;校验 events 指针必须落在应用高半空间
图说:一条「画得快 → 测 → 少画 → 等得省」的应用性能线,贯穿全章。
3. 核心原理¶
3.1 exit() 的最后一公里¶
C 标准规定 exit() 会调用 atexit 注册的函数(启动例程或库内部可能注册),
所以不能只调 SyscallExit——要让 Newlib 的 exit() 原样可用,
给 newlib_support.c 补一个 _exit()(内部转调 SyscallExit)即可1。
「是不是有点儿太简单了,有点儿扫兴?」——系统的成熟,就是把神奇变成应当。
3.2 画点、画星、画线¶
- 画点 = 画 1×1 的矩形:专门造 DrawPixel 不如直接复用 FillRectangle, 「你让它可以绘制矩形,而不仅是使用点绘制系统调用,这一点非常有用」2;
- 星空 = 黑底 + 伪随机坐标批量画小矩形3;
- 直线的数学:斜率 m = (y1−y0)/(x0→x1);|m|>1(陡于 45°)时沿 x 走会断点, 要交换 x/y 角色;端点相同时按画点处理(不然斜率除零)4。 lines 应用拿它画辐射状星芒:每 5° 一根5。
3.3 计时与重绘抑制:纪律进系统调用¶
SyscallGetCurrentTick / SyscallGetTimerFrequency:应用第一次能给自己掐表——
画 100 颗星前后取 tick,差值除以频率就是毫秒6。
测出来慢,原因:每个绘制系统调用都自动全窗重绘。
修法:所有绘制类系统调用加 layer_flags 参数,第 0 位置 1 = 本次不重绘;
画完手动 WinRedraw() 一次。默认位 0 = 自动重绘,旧应用行为不变(兼容性)7。
——第 9 章(阴影缓冲)、第 15 章(DrawArea)的「先测量,再优化」,
这次以 API 设计的形式固化下来。
3.4 ReadEvent:应用的「阻塞等待」¶
winhello 的问题:一启动就画完、关窗、退出,什么都看不见。
要它活着等输入,得有一个「等事件」的系统调用8。
设计取舍被明白摊开:轮询(没事件也立即返回,应用死循环问)浪费 CPU;
阻塞(没事件就让应用睡,来事件再叫醒)才是正解9。
ReadEvent(events[], len):队列里有事件就翻译进数组立即返回;没有就让任务休眠
(第 14 章 Sleep/Wakeup 原语的原样复用)——等待期间 CPU 占用永远不接近 100%10。
事件源顺势复用旧机制:应用跑在终端任务里,按键消息本来就发进终端任务的队列;
ReadEvent 从该队列取消息、翻译成 AppEvent(kQuit 等类型)再写给应用11。
安全规矩在此立起:校验 events 指针必须落在应用的高半地址空间——
恶意应用传个小数值想借系统调用踩内核内存,直接报错12。
应用侧的用法变成:循环 ReadEvent,收到 kQuit 就 CloseWindow + exit13。
4. 作者的判断与证据¶
判断:API 设计带着性能观。 重绘抑制不是「画完了发现慢再改内核」, 而是把测量(msec 计时)与批量绘制配成一个完整循环,边开 API 边立规矩。
判断:复用旧机制优先造新机制。 事件源不新设管道,直接读终端任务的消息队列 (16.1 的 layer_task_map 一路服务到这里);等待不造新原语,复用 Sleep/Wakeup。
证据: 星空与星芒截图、计时读数、重绘开关的前后对比; CPU 占用「永远不会接近 100%」是阻塞语义的直接推论。
5. 边界与局限¶
- AppEvent 类型只有 kQuit 一个,鼠标/定时器事件要到下一章;
- ReadEvent 一次调用内的事件队列没有上限语义(len 由应用给,满了就返回读到的);
- 重绘抑制是全局开关语义的雏形,不是脏矩形(damage tracking)系统;
- 指针校验只查了高半/低半,没有按应用页表逐项验证(够教学,不够安全审计)。
6. 可带走的¶
本章主线例子,一行写完: 星空应用(画点=画矩形 + 计时器掐表 + 抑制自动重绘 + 手动 WinRedraw)→ 直线(陡线换轴)→ CloseWindow → ReadEvent 阻塞等 Ctrl+Q。
- 点 = 1×1 矩形:能画矩形就不用单独造画点;
- 陡直线(|m|>1)要交换 x/y 轴再逐点推进;端点相同按画点(防除零);
- 「先测量,再优化」进了应用层:计时器值做成系统调用;
- 批量绘制的性能开关 = 默认自动重绘 + flags 关闭 + 手动 Redraw,兼容性靠默认值;
- 轮询浪费 CPU,阻塞让出 CPU;等待原语直接复用任务的 Sleep/Wakeup;
- 事件源可以借道任务消息队列——新机制先问旧机制能不能扛;
- 系统调用的指针参数要校验范围(高半空间),防恶意应用借刀杀人。
7. 原文地图¶
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| exit() 与 _exit | 22.1 使用exit()(osbook_day22a) | text/142-ch22-01-22-1-exit-osbook-day22a.txt:9(搜「exit()」) · text/142-ch22-01-22-1-exit-osbook-day22a.txt:22(搜「_exit()」) |
| 画点=画矩形与星空 | 22.2 绘制两个点(osbook_day22b) | text/143-ch22-02-22-2-osbook-day22b.txt:13(搜「矩形而非点」) · text/143-ch22-02-22-2-osbook-day22b.txt:68(搜「夜空」) |
| 计时器值系统调用 | 22.3 获取计时器值(osbook_day22c) | text/144-ch22-03-22-3-osbook-day22c.txt:19(搜「先测量后优化」) · text/144-ch22-03-22-3-osbook-day22c.txt:31(搜「获取计时器值和频率」) |
| 重绘抑制与 WinRedraw | 22.4 窗口绘制的优化(osbook_day22d) | text/145-ch22-04-22-4-osbook-day22d.txt:5(搜「不自动重绘」) · text/145-ch22-04-22-4-osbook-day22d.txt:20(搜「第0位」) · text/145-ch22-04-22-4-osbook-day22d.txt:22(搜「WinRedraw」) |
| 直线的斜率两种情况 | 22.5 绘制直线(osbook_day22e) | text/146-ch22-05-22-5-osbook-day22e.txt:35(搜「斜率」) · text/146-ch22-05-22-5-osbook-day22e.txt:43(搜「大于1」) · text/146-ch22-05-22-5-osbook-day22e.txt:63(搜「分母将为0」) |
| lines 辐射状星芒 | 22.5 绘制直线(osbook_day22e) | text/146-ch22-05-22-5-osbook-day22e.txt:82(搜「lines」) |
| CloseWindow | 22.6 关闭窗口(osbook_day22f) | text/147-ch22-06-22-6-osbook-day22f.txt:30(搜「关闭窗口的系统调用」) · text/147-ch22-06-22-6-osbook-day22f.txt:48(搜「RemoveLayer」) |
| 轮询 vs 阻塞 | 22.7 等待按键输入(osbook_day22g) | text/148-ch22-07-22-7-osbook-day22g.txt:24(搜「轮询」) · text/148-ch22-07-22-7-osbook-day22g.txt:27(搜「后一种行为」) · text/148-ch22-07-22-7-osbook-day22g.txt:24(搜「100%」) |
| AppEvent 与消息队列借道 | 22.7 等待按键输入(osbook_day22g) | text/148-ch22-07-22-7-osbook-day22g.txt:37(搜「kQuit」) · text/148-ch22-07-22-7-osbook-day22g.txt:61(搜「layer_task_map」) · text/148-ch22-07-22-7-osbook-day22g.txt:64(搜「消息队列」) |
| 指针校验 | 22.7 等待按键输入(osbook_day22g) | text/148-ch22-07-22-7-osbook-day22g.txt:75(搜「前半部」) |
-
出处:「22.1 使用exit()」(
text/142-ch22-01-22-1-exit-osbook-day22a.txt:9,搜「exit()」;:22,搜「_exit()」)。原文说明:要用 C 标准库的 exit() 就须为它准备 _exit(),「是不是有点儿太简单了,有点儿扫兴?」 ↩ -
出处:「22.2 绘制两个点」(
text/143-ch22-02-22-2-osbook-day22b.txt:13,搜「矩形而非点」;:68,搜「夜空」)。 ↩ -
出处:「22.2 绘制两个点」(
text/143-ch22-02-22-2-osbook-day22b.txt:68,搜「夜空」)。星空 = 黑底加伪随机坐标的小矩形,为演示批量绘制而设。 ↩ -
出处:「22.5 绘制直线」(
text/146-ch22-05-22-5-osbook-day22e.txt:35,搜「斜率」;:43,搜「大于1」;:62,搜「分母将为0」)。 ↩ -
出处:「22.5 绘制直线」(
text/146-ch22-05-22-5-osbook-day22e.txt:82,搜「lines」)。 ↩ -
出处:「22.3 获取计时器值」(
text/144-ch22-03-22-3-osbook-day22c.txt:19,搜「先测量后优化」;:31,搜「获取计时器值和频率」)。 ↩ -
出处:「22.4 窗口绘制的优化」(
text/145-ch22-04-22-4-osbook-day22d.txt:5,搜「不自动重绘」);:19,搜「第0位」;:22,搜「WinRedraw」)。默认不置位 = 自动重绘,兼容性靠默认值。 ↩ -
出处:「22.7 等待按键输入」(
text/148-ch22-07-22-7-osbook-day22g.txt:14,搜「22.7 等待按键输入」;:16,搜「Ctrl+Q」)。 ↩ -
出处:「22.7 等待按键输入」(
text/148-ch22-07-22-7-osbook-day22g.txt:24,搜「轮询」;:27,搜「后一种行为」;:49,搜「100%」)。 ↩ -
出处:「22.7 等待按键输入」(
text/148-ch22-07-22-7-osbook-day22g.txt:55,搜「SyscallReadEvent()等待」;:48,搜「100%」)。 ↩ -
出处:「22.7 等待按键输入」(
text/148-ch22-07-22-7-osbook-day22g.txt:61,搜「layer_task_map」;:64,搜「消息队列」)。应用跑在终端任务里,按键消息本来就发进该任务的队列。 ↩ -
出处:「22.7 等待按键输入」(
text/148-ch22-07-22-7-osbook-day22g.txt:75,搜「前半部」;:67,搜「破坏操作系统的内存区域」)。 ↩ -
出处:「22.7 等待按键输入」(
text/148-ch22-07-22-7-osbook-day22g.txt:37,搜「kQuit」;:41,搜「退出事件」)。 ↩