跳转至

定时器与睡眠 — 从 jiffy 到 POSIX timer

这一章讲什么: 「睡到某个时刻再醒」和「干活干到一半被定时器 打断一下」是两类不同的问题。本章把支撑它们的地基全挖出来: 硬件时钟怎么 ticking(为什么老内核的软件定时器精度到一个 jiffy)、 sleep 家族暗藏的共享定时器冲突、nanosleep 重启动带来的 timer drift、以及正解——绝对时间的 clock_nanosleep 和 POSIX timer。 项目:两版终端进度条(alarm 版 → POSIX timer 版)和一个文件 监视器 watchfiles,把第 8 章的信号军规全部实战一遍。

1. 顶层全景:时间从哪来,定时器到哪去

  硬件层:RTC(电池保持的 wall clock,开机校时用)
          PIT(可编程间隔定时器,内核约 1000Hz 发 tick)
          TSC/HPET(高精度计数器,内核 2.6 起的高分辨率定时器底座)
      │  tick 的间隔 = 1 个 jiffy(值随内核配置,1–10ms)
      ▼
  系统时钟:纯软件,开机用 RTC 或 NTP 校一次,之后靠数 PIT 的 tick 前进
      │  老内核上软件定时器精度 ≤ jiffy;新内核走 HPET 高精度定时器
      ▼
  睡眠函数:sleep/usleep/nanosleep/clock_nanosleep
      │  被打断 → EINTR → 余量重睡 → timer drift(相对时间累积误差)
      │  绝对睡眠(TIMER_ABSTIME)→ 免疫 drift
      ▼
  定时器:alarm(1 秒粒度、一次性)
      │  → setitimer/getitimer(1970s–80s,2008 已废弃)
      │  → POSIX timer:timer_create + timer_settime + timer_getoverrun
      ▼
  通知路径:信号(SIGEV_SIGNAL)/ 线程回调(SIGEV_THREAD)/ 不通知

图说:越往下分辨率越高、越能精确控制;代价是 API 越繁琐。 主线例子:同一 6.789 秒的睡眠,相对睡法漂移 0.42 毫秒、绝对睡法 漂移 0.09 毫秒——数字来自原书实测,这一章的机理全藏在这两个数里。

2. 地基:硬件时钟、jiffy 与系统时钟

计时的硬件家当1: RTC(real-time clock,电池保持 wall clock, 关机也走;Linux 只在开机时拿它初始化软件时间;还能编程 2–8192Hz 周期中断、当闹钟用);PIT(programmable interval timer,内核 通常编程成约 1000Hz 发中断,这个中断叫 timer interrupt,间隔 就是 1 个 tick;内核常量 HZ 是 tick 频率,jiffy 是一个 tick 的时长——100Hz 内核 jiffy=0.01s,250Hz 内核 0.004s);更精的 有 TSC(时间戳计数器,振荡频率可达 1GHz)和 HPET(高精度 事件定时器,内部计数 ≥100KHz,内核 2.6 起支持)。

系统时钟是纯软件时钟:开机时从 RTC 或 NTP 服务校时,此后每次 PIT 中断到来就累加——「靠数硬件定时器的 tick 过日子」2。 这事和编程的关系一句话:老内核上软件定时器的精度天花板就是 jiffy;新内核走 HPET 高精度定时器,不受这个限3。

3. 睡眠函数:共享定时器的暗雷与 nanosleep

sleep/usleep 的隐藏问题:它们背后共用一个软件定时器,还和对 alarm() 共用的那个是同一个——重叠调用互相干扰,还会和信号的产生/交付 打架。man 页的建议:改用 nanosleep()4。

nanosleep(&req, &rem) 不碰信号、纳秒分辨率;被打断时返 -1/EINTR, rem 里是没睡完的余量,标准重睡循环是:

do {
    retval = nanosleep(&initial_sleep, &remainder);
    if ( retval == -1 && errno != EINTR ) fatal_error(errno, "nanosleep");
    else initial_sleep = remainder;        /* 接着睡剩下的 */
} while ( retval < 0 );

主线例子:drift 实测

nanosleep_demo2 睡 6.789123456 秒、中途打了 4 个信号:

Sleep lasted 6.7895459130 seconds, 0.0004224570 seconds longer than requested.

信号加到 14 个,漂移涨到 0.00118 秒;32 个,0.00358 秒。 机理:每次从 handler 返回再重新入睡,请求的时长是相对量,起点 每次往后挪一点——重启动的次数越多,欠账越多,这就是 timer drift5。

4. clock_nanosleep:绝对时间免疫 drift——以及一个著名陷阱

clock_nanosleep(clockid, flags, &request, &remain) 多两个参数: 选时钟(CLOCK_REALTIME / CLOCK_MONOTONIC),以及 TIMER_ABSTIME ——把 request 解释为「睡到时钟到达这个绝对时刻」,像设闹钟: 现在 11 点、设 15 点,就睡到 15 点,不是睡 15 小时6。要睡相对 时长,先 clock_gettime 拿到现在、加上间隔、再把目标时刻传进去。 绝对睡法被打断后,用同一个目标时刻重睡——起点永远不变,漂移 只剩调度噪声:同样的实验,绝对版漂移 0.0000911 秒,30 个信号 打断后仍稳定在 0.00012 秒7。

陷阱(书里加框强调):nanosleep 被信号打断返回 -1 并置 errno=EINTR;clock_nanosleep 被信号打断直接返回正数错误码 EINTR。判断写法完全不同:

retval = clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &end, NULL);
if ( retval == EINTR )      /* 被打断,用同一 end 再睡 */
else if ( retval > 0 )      /* 真错误 */
else                        /* retval==0,睡到点了 */

两套返回值约定混用,是这段 API 最著名的坑8。

5. alarm():最老的定时器

alarm(秒) 安排 seconds 秒后给自己发 SIGALRM;精度 1 秒; 一次性;再调一次就把前一个冲掉(返回上一个剩余秒数: alarm(10) 后 4 秒再 alarm(20) → 返回 6);alarm(0) 取消9。 妙用:给长任务设「超时保险」——到点没干完就收到 SIGALRM 收工。

项目一:进度条 v1(alarm 版)

终端进度条的刷新机制,三件小事全是学问:

  1. 同行动画:行首写 \r(回车,光标挪回行首)再画整行,新行 盖旧行;行尾换行 \n 会把光标推到下一行。
  2. 行缓冲:printf 不带 \n 不会立刻出——C 库对终端是行缓冲; 所以 handler 里 printf(...); fflush(stdout); 必须成对10。
  3. handler 里续钟:alarm 是一次性的,要在 SIGALRM handler 里 alarm(REFRESH_INTERVAL) 才能周期刷新——每次 handler 开跑到 重新上钟之间的延迟会累积(「下次更晚一点」),这就是 alarm 当 周期定时器的先天不足11。

填充条用两个 memset(先全填 - 再把左端 finished_work 个改 #); 完成比例存在文件作用域 fraction_completed(handler 和主程序 都要看);主程序更新它时用 sigprocmask 挡 SIGALRM 防竞争。 CTRL-C/CTRL-\ 的 handler:擦掉条 → raise(SIGTERM) 自杀——又是 第 8 章那套「清理后按默认动作办」12。

6. POSIX timer:正规军的玩法

三件套(-lrt 链接,glibc 2.17 起并入 libc):

timer_create(clockid, &sigevent, &timerid);   /* 造一个未上膛的定时器 */
timer_settime(timerid, flags, &newval, &old); /* 上膛/重设/关(it_value=0) */
timer_delete(timerid);                        /* 销掉 */

struct itimerspec { it_value; it_interval; }——初值(首响时间) 与周期(0 = 单发,非 0 = 每周期自动重装,真正的周期定时器); flags 同样支持 TIMER_ABSTIME(初值按绝对时刻解释)13。

sigevent 的四种通知方式(POSIX timer 比 alarm 强的地方):

sigev_notify 到期时
SIGEV_SIGNAL 发 sigev_signo 指定的信号;handler 的 si_code==SI_TIMER,si_value 可带 timerid
SIGEV_NONE 不通知(用来「让时钟跑着」)
SIGEV_THREAD 开一个新线程跑 sigev_notify_function(sigev_value)
SIGEV_THREAD_ID 信号发给指定线程(Linux 特有)

timer overrun:通知丢了几个

信号不排队的规则(第 8 章)对定时器同样成立:每个定时器在进程 里最多挂起 1 个通知,处理不过来就丢——丢了几个叫 overrun, timer_getoverrun(timerid) 报数(handler 里也能调)。意义:就算 通知丢了,计数还在,「每次通知该 +1」的场景能补上欠账14。

项目二:进度条 v2(POSIX timer 版)

三处升级:通知改 SIGUSR1(免得和别人的 SIGALRM 抢);刷新间隔 用户可定(0.001 秒到 8 秒);模拟任务里的小睡改用 clock_nanosleep(TIMER_ABSTIME)——小节间隔是固定绝对时刻, 周期刷新不再被「越睡越短」扭曲,也没有 overrun 累积15。 收尾的清理顺序也值得抄:任务完 → 等一拍(给最后一次刷新机会)→ timer_settime 把 it_value 清零关定时器 → SIG_IGN 通知信号 → 擦条 → 打印 Done16。

7. 项目三:watchfiles——文件监视器

监视一组文件的大小,变了就刷新屏幕,稳定几拍后自动退出,q 键 提前退。新知识点:

  • ANSI 转义序列:\033[(CSI)开头,控制终端行为—— \033[2J 清屏、\033[1;1H 光标回左上角、\033[<行>;1H 移到指定行。所有终端模拟器都认,这就是 ncurses(第 19 章) 出现之前的原始做法17。
  • handler 全 async-signal-safe:不碰 printf/strcat 之外的 stdio——先 strcpy/strcat 拼出整屏字符串,一次 write 打出去; 屏幕布局 = 「刷新区(每拍重建)+ 静态区(提示行)」18。
  • 状态全靠文件作用域:size 数组、stable_cnt、stopflag (volatile sig_atomic_t);主循环 read 等 q,被信号打断返 EINTR 就 continue;退出前 tcflush 清掉残留的换行19。

8. 作者的判断与证据

有明证的:

  • drift 两组对照数据(相对 0.42ms/4 信号 → 0.0036s/32 信号; 绝对恒 0.09–0.12ms)是同机实测57。
  • clock_nanosleep 返回值不返 -1 的差异,加框强调并给了正确 判法8。
  • overrun 的「每定时器最多挂起一个」语义出自 man 页14。

作者的判断:

  • 「alarm 只适合教学,周期刷新上 POSIX timer」——由 drift 数据 支撑的工程结论15。
  • watchfiles 里「每拍整屏重建字符串再一次 write」优于「逐字段 定位改」——实现简洁性优先,理直气壮的取舍18。
  • 「先挡信号再更新共享变量」的用法在第 8 章是军规,本章是实际 需求——教学编排的痕迹,但理由真实12。

9. 边界与局限

  • 实时信号(SIGRTMIN 起的带数据、可排队信号)只在 sigevent 一节捎带,没展开——那是信号当「队列」用的进阶,本书点到为止;
  • setitimer/getitimer 只给了「已废弃,用 POSIX timer」的迁移路径, 没展开13;
  • watchfiles 的屏幕布局假设文件数 < 行数(拿不到终端行数, 那是第 18 章的 ioctl);也没有用 ncurses;
  • 精度讨论止步「HPET/HRT 之后不受 jiffy 限」,没讲内核时钟源 切换细节3。

10. 可带走的

主线例子,一行复述: 睡 6.789123456s 中途来 4 个信号——相对 重睡漂移 +0.42ms(次数越多欠越多),绝对睡眠(TIMER_ABSTIME 固定目标时刻)漂移稳定 +0.09ms。

  1. jiffy = PIT tick 的间隔(HZ 分之一),老内核软件定时器精度 的上限;HPET 高精度定时器之后不受此限。
  2. sleep/usleep/alarm 共用一个软件定时器,会互相干扰;认真 用 nanosleep。
  3. nanosleep 被打断返 -1/EINTR,rem 拿余量;重睡循环要 do-while。
  4. 相对重睡累积 timer drift;要稳用 clock_nanosleep 的 TIMER_ABSTIME(先 gettime 加间隔)。
  5. clock_nanosleep 打断返回正数 EINTR 不返 -1——判断写法 和 nanosleep 不同!
  6. alarm(秒)= N 秒后 SIGALRM;再调冲掉旧的(返回剩余), alarm(0) 取消;handler 里续钟可当周期用,但有累积延迟。
  7. 终端同行刷新 = \r 回行首重画;printf 要 fflush(行缓冲)。
  8. POSIX timer:it_value 初值、it_interval 周期(0=单发)、 it_value=0 关;SIGEV_SIGNAL/THREAD/NONE 通知可选。
  9. timer overrun = 丢的通知数,handler 里 timer_getoverrun 补账。
  10. handler 里做屏幕更新:只碰 async-signal-safe 函数,拼好 字符串一次 write;屏幕分刷新区+静态区。

11. 原文地图

主题 原书章 原文位置
RTC/PIT/TSC/HPET 与 jiffy Chapter 9: Timers and Sleep Functions text/15-ch09-chapter-9-timers-and-sleep-functions.txt:144(搜「real-time clock」) · :166(搜「metronome」) · :169(搜「jiffy」)
系统时钟靠 tick 前进 同上 :192(搜「every time it receives」)
sleep/usleep 共享定时器问题 同上 :221(搜「share the same timer」) · :225(搜「nanosleep」)
nanosleep 与重睡循环 同上 :232(搜「int nanosleep」) · :411(搜「do」)
drift 实测数据 同上 :512(搜「0.0004224570」) · :519(搜「0.0035780350」)
clock_nanosleep 绝对睡眠 同上 :561(搜「alarm clock」) · :589(搜「t2, not t1 + t2」)
返回值陷阱(EINTR 非 -1) 同上 :616(搜「returns the error code EINTR」)
绝对版 drift 对照 同上 :674(搜「0.0000910600」)
alarm 语义与取消 同上 :705(搜「SIGALRM」) · :707(搜「canceled」)
进度条 v1 三机制 同上 :1066(搜「return character」) · :1093(搜「fflush」) · :923(搜「race condition」)
竞争与 sigprocmask 保护 同上 :947(搜「sigprocmask(SIG_BLOCK」)
itimerspec 与 timer_settime 同上 :1369(搜「struct itimerspec」) · :1377(搜「disarmed」)
sigevent 四种通知 同上 :1293(搜「SIGEV_NONE」) · :1298(搜「SIGEV_THREAD」)
timer overrun 同上 :1402(搜「at most one signal」) · :1406(搜「timer_getoverrun」)
进度条 v2 的绝对睡眠改造 同上 :1519(搜「clock_nanosleep(CLOCK_MONOTONIC」) · :1647(搜「timer_settime」)
ANSI 转义序列 同上 :1910(搜「Control Sequence Introducer」) · :1913(搜「erases the entire」)
watchfiles 的 handler 布局 同上 :2058(搜「update_stats」) · :2097(搜「write(STDOUT_FILENO」)

  1. 出处:「Chapter 9: Timers and Sleep Functions」第 143–182 段(text/15-ch09-chapter-9-timers-and-sleep-functions.txt:144,搜「real-time clock」) · :166(搜「metronome」) · :169(搜「jiffy」)。 ↩

  2. 出处:「Chapter 9: Timers and Sleep Functions」第 183–205 段(text/15-ch09-chapter-9-timers-and-sleep-functions.txt:192,搜「every time it receives」)。 ↩

  3. 出处:「Chapter 9: Timers and Sleep Functions」第 200–205 段(text/15-ch09-chapter-9-timers-and-sleep-functions.txt:201,搜「resolution of software timers」)。 ↩↩

  4. 出处:「Chapter 9: Timers and Sleep Functions」第 207–225 段(text/15-ch09-chapter-9-timers-and-sleep-functions.txt:221,搜「share the same timer」) · :225(搜「nanosleep() instead」)。 ↩

  5. 出处:「Chapter 9: Timers and Sleep Functions」第 505–523 段(text/15-ch09-chapter-9-timers-and-sleep-functions.txt:512,搜「0.0004224570」) · :519(搜「32 signals」)。 ↩↩

  6. 出处:「Chapter 9: Timers and Sleep Functions」第 558–570 段(text/15-ch09-chapter-9-timers-and-sleep-functions.txt:561,搜「alarm clock」)。 ↩

  7. 出处:「Chapter 9: Timers and Sleep Functions」第 665–682 段(text/15-ch09-chapter-9-timers-and-sleep-functions.txt:674,搜「0.0000910600」)。 ↩↩

  8. 出处:「Chapter 9: Timers and Sleep Functions」第 612–626 段(text/15-ch09-chapter-9-timers-and-sleep-functions.txt:616,搜「returns the error code EINTR」)。 ↩↩

  9. 出处:「Chapter 9: Timers and Sleep Functions」第 690–736 段(text/15-ch09-chapter-9-timers-and-sleep-functions.txt:705,搜「SIGALRM」) · :707(搜「canceled」)。 ↩

  10. 出处:「Chapter 9: Timers and Sleep Functions」第 1060–1075 段(text/15-ch09-chapter-9-timers-and-sleep-functions.txt:1066,搜「return character」)与第 1076–1096 段的行缓冲框(text/15-ch09-chapter-9-timers-and-sleep-functions.txt:1093,搜「fflush」)。 ↩

  11. 出处:「Chapter 9: Timers and Sleep Functions」第 1203–1212 段(text/15-ch09-chapter-9-timers-and-sleep-functions.txt:1208,搜「cumulative delays」)。 ↩

  12. 出处:「Chapter 9: Timers and Sleep Functions」第 923–951 段(text/15-ch09-chapter-9-timers-and-sleep-functions.txt:947,搜「sigprocmask(SIG_BLOCK」)与第 1148–1156 段的终止 handler。 ↩↩

  13. 出处:「Chapter 9: Timers and Sleep Functions」第 1214–1242 段(text/15-ch09-chapter-9-timers-and-sleep-functions.txt:1220,搜「obsolete」)与第 1367–1393 段(text/15-ch09-chapter-9-timers-and-sleep-functions.txt:1369,搜「struct itimerspec」)。 ↩↩

  14. 出处:「Chapter 9: Timers and Sleep Functions」第 1395–1434 段(text/15-ch09-chapter-9-timers-and-sleep-functions.txt:1402,搜「at most one signal」) · :1406(搜「timer_getoverrun」)。 ↩↩

  15. 出处:「Chapter 9: Timers and Sleep Functions」第 1442–1533 段(text/15-ch09-chapter-9-timers-and-sleep-functions.txt:1519,搜「clock_nanosleep(CLOCK_MONOTONIC」)。 ↩↩

  16. 出处:「Chapter 9: Timers and Sleep Functions」第 1593–1662 段(text/15-ch09-chapter-9-timers-and-sleep-functions.txt:1647,搜「timer_settime」)。 ↩

  17. 出处:「Chapter 9: Timers and Sleep Functions」第 1900–1964 段(text/15-ch09-chapter-9-timers-and-sleep-functions.txt:1910,搜「Control Sequence Introducer」) · :1913(搜「erases the entire」)。 ↩

  18. 出处:「Chapter 9: Timers and Sleep Functions」第 1968–2033 段(text/15-ch09-chapter-9-timers-and-sleep-functions.txt:2005,搜「async-signal-safe」)与第 2058–2107 段的 handler(text/15-ch09-chapter-9-timers-and-sleep-functions.txt:2097,搜「write(STDOUT_FILENO」)。 ↩↩

  19. 出处:「Chapter 9: Timers and Sleep Functions」第 2140–2183 段(text/15-ch09-chapter-9-timers-and-sleep-functions.txt:2170,搜「while ( !stopflag )」) · :2177(搜「tcflush」)。 ↩