跳转至

应用的图形与事件(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×1 矩形:能画矩形就不用单独造画点;
  2. 陡直线(|m|>1)要交换 x/y 轴再逐点推进;端点相同按画点(防除零);
  3. 「先测量,再优化」进了应用层:计时器值做成系统调用;
  4. 批量绘制的性能开关 = 默认自动重绘 + flags 关闭 + 手动 Redraw,兼容性靠默认值;
  5. 轮询浪费 CPU,阻塞让出 CPU;等待原语直接复用任务的 Sleep/Wakeup;
  6. 事件源可以借道任务消息队列——新机制先问旧机制能不能扛;
  7. 系统调用的指针参数要校验范围(高半空间),防恶意应用借刀杀人。

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(搜「前半部」)

  1. 出处:「22.1 使用exit()」(text/142-ch22-01-22-1-exit-osbook-day22a.txt:9,搜「exit()」;:22,搜「_exit()」)。原文说明:要用 C 标准库的 exit() 就须为它准备 _exit(),「是不是有点儿太简单了,有点儿扫兴?」 ↩

  2. 出处:「22.2 绘制两个点」(text/143-ch22-02-22-2-osbook-day22b.txt:13,搜「矩形而非点」;:68,搜「夜空」)。 ↩

  3. 出处:「22.2 绘制两个点」(text/143-ch22-02-22-2-osbook-day22b.txt:68,搜「夜空」)。星空 = 黑底加伪随机坐标的小矩形,为演示批量绘制而设。 ↩

  4. 出处:「22.5 绘制直线」(text/146-ch22-05-22-5-osbook-day22e.txt:35,搜「斜率」;:43,搜「大于1」;:62,搜「分母将为0」)。 ↩

  5. 出处:「22.5 绘制直线」(text/146-ch22-05-22-5-osbook-day22e.txt:82,搜「lines」)。 ↩

  6. 出处:「22.3 获取计时器值」(text/144-ch22-03-22-3-osbook-day22c.txt:19,搜「先测量后优化」;:31,搜「获取计时器值和频率」)。 ↩

  7. 出处:「22.4 窗口绘制的优化」(text/145-ch22-04-22-4-osbook-day22d.txt:5,搜「不自动重绘」);:19,搜「第0位」;:22,搜「WinRedraw」)。默认不置位 = 自动重绘,兼容性靠默认值。 ↩

  8. 出处:「22.7 等待按键输入」(text/148-ch22-07-22-7-osbook-day22g.txt:14,搜「22.7 等待按键输入」;:16,搜「Ctrl+Q」)。 ↩

  9. 出处:「22.7 等待按键输入」(text/148-ch22-07-22-7-osbook-day22g.txt:24,搜「轮询」;:27,搜「后一种行为」;:49,搜「100%」)。 ↩

  10. 出处:「22.7 等待按键输入」(text/148-ch22-07-22-7-osbook-day22g.txt:55,搜「SyscallReadEvent()等待」;:48,搜「100%」)。 ↩

  11. 出处:「22.7 等待按键输入」(text/148-ch22-07-22-7-osbook-day22g.txt:61,搜「layer_task_map」;:64,搜「消息队列」)。应用跑在终端任务里,按键消息本来就发进该任务的队列。 ↩

  12. 出处:「22.7 等待按键输入」(text/148-ch22-07-22-7-osbook-day22g.txt:75,搜「前半部」;:67,搜「破坏操作系统的内存区域」)。 ↩

  13. 出处:「22.7 等待按键输入」(text/148-ch22-07-22-7-osbook-day22g.txt:37,搜「kQuit」;:41,搜「退出事件」)。 ↩