Linux time function in C/C++【2】
Linux 时间与定时机制设计思想的核心
一、时间函数的分类与作用
Linux 的时间函数大体分为两类:
(1)计时函数(获取当前时间)
这些函数用于读取当前的系统时间或高精度时间戳,区别在于时间单位与精度不同:
| 函数 | 返回结构 | 精度 | 说明 |
|---|---|---|---|
time() | time_t(整型秒) | 秒级 | 最简单、最老的API |
ftime() | struct timeb | 毫秒级 | 已被废弃 |
gettimeofday() | struct timeval | 微秒级 | 常用,用户态实现,高效 |
clock_gettime() | struct timespec | 纳秒级 | 最精确,但调用代价略高 |
(2)定时函数(控制程序等待或定时触发)
这些函数用于让程序等待一定时间或设置定时器,常用于超时控制、周期任务等:
| 函数 | 类型 | 是否线程安全 | 是否基于信号 | 是否可与 I/O 复用机制结合 |
|---|---|---|---|---|
sleep() | 秒级休眠 | 否 | 是(SIGALRM) | 否 |
usleep() | 微秒级休眠 | 否 | 是(SIGALRM) | 否 |
nanosleep() | 纳秒级休眠 | 是 | 否 | 否 |
alarm() | 设置闹钟信号 | 否 | 是(SIGALRM) | 否 |
getitimer() / setitimer() | 定时器信号 | 否 | 是 | 否 |
timer_create() / timer_settime() | POSIX 定时器 | 是 | 可选信号 | 否 |
timerfd_create() / timerfd_settime() | 文件描述符定时器 | 是 | 否 | 可与 epoll/poll 集成 |
timerfd_* 系列,能直接与 Reactor 模型兼容。
二、为何选择 gettimeofday() 而不是 clock_gettime()
原因 1:精度与开销平衡
time()只能到秒,不够;ftime()已被淘汰;clock_gettime()虽能到纳秒,但调用属于系统调用,会陷入内核,开销较高;gettimeofday()精度达微秒,且 在 x86-64 平台是纯用户态实现(无系统调用陷入),因此速度极快。
也就是说:
clock_gettime()追求极致精度;
gettimeofday()追求低开销和足够的精度(微秒级即可满足绝大多数需求)。
原因 2:实现机制优越
在 x86-64 平台上,gettimeofday() 的实现利用了 VDSO(Virtual Dynamic Shared Object)机制,让用户态程序可直接访问内核的共享时间数据结构,无需切换上下文。
三、为何选择 timerfd_* 而非其他定时函数
原因 1:避免信号混乱
-
sleep()、usleep()、alarm()、getitimer()、setitimer()等函数都依赖 SIGALRM 信号。 -
多线程程序中,信号机制非常危险:
- 不确定信号由哪个线程处理;
- 信号处理函数受限(不可调用大部分系统API);
- 若主程序与第三方库都用 SIGALRM,极易发生冲突。
强调:多线程程序应尽量避免信号驱动定时。
-
不确定信号由哪个线程处理:
- 由于信号处理是异步且不可预测的,操作系统可能将信号随机分配给任意线程
- 主线程和工作线程都可能成为信号处理者,导致程序行为不可控
- 例如在Web服务器中,SIGTERM信号可能被处理请求的工作线程捕获,而非主线程
- POSIX标准规定信号的处理线程是未指定的,不同实现表现各异
-
信号处理会中断线程的正常执行流:
- 信号处理函数可能在任何时刻抢占线程执行
- 若信号发生在临界区内,可能破坏共享数据的一致性
- 比如数据库写入操作被信号中断,导致数据损坏
-
线程安全信号处理难度大:
- 信号处理函数中只能使用异步信号安全函数
- 常规的锁机制在信号处理中不可用
- 需要复杂的线程间协调机制来保证安全
-
信号处理与线程取消的交互问题:
- 信号可能意外触发线程取消点
- 导致资源清理和状态恢复更加困难
- 在实时系统中这类问题尤为突出
原因 2:非阻塞编程要求“事件驱动”
nanosleep()是线程安全的,但会让线程挂起;- 会造成事件循环停滞;
- 正确做法:用定时器注册回调函数,让事件循环继续执行。
原因 3:timerfd 与 epoll完美结合
timerfd_create()创建的定时器表现为一个文件描述符(fd);- 当定时到期,该 fd 就会“变得可读”;
- 因此它能直接与
select()、poll()、epoll()结合; - 统一处理网络I/O事件与定时事件,形成优雅一致的事件循环机制。
原因 4:精度更高
epoll_wait()和poll()的 timeout 参数是毫秒级;- 而
timerfd_settime()可达 纳秒级 精度。
因此,timerfd 提供了高精度、统一接口、线程安全的定时解决方案。
四、Linux 定时与计时的现实限制
在非实时 Linux 系统中,不可能做到“绝对精确”的计时。
原因是:
- Linux 是非实时调度系统;
- 任意时刻,内核可能把当前任务换出;
- 尤其在 CPU 负载高时,误差更明显。
因此我们能做的只是提高“时间精度的可靠性”,
比如:
- 控制 CPU 负载;
- 使用合适的优先级;
- 利用高精度 timerfd;
- 保证程序在“99.99% 的情况下”按预期执行。
五、精度 vs 分辨率
最后两者区别:
- Resolution(分辨率):时间函数能“分出多细的时间单位”,比如微秒或纳秒;
- Accuracy(准确度):返回值与真实时间的偏差;
举例:
你可能有一个 1 微秒分辨率的时钟,但在高负载时偏差达 100 微秒,这意味着分辨率高但准确度低。
六、总结对比表
| 功能类别 | 典型函数 | 精度 | 是否阻塞线程 | 是否依赖信号 | 是否线程安全 | 是否适合Reactor |
|---|---|---|---|---|---|---|
| 计时 | gettimeofday() | 微秒 | 否 | 否 | 是 | ✅ |
| 定时 | sleep() | 秒 | ✅ | ✅ | 否 | ❌ |
| 定时 | nanosleep() | 纳秒 | ✅ | 否 | ✅ | ❌ |
| 定时 | getitimer() | 微秒 | ✅ | ✅ | 否 | ❌ |
| 定时 | timer_create() | 纳秒 | 否 | ✅可控 | ✅ | ❌ |
| 定时 | timerfd_create() | 纳秒 | 否 | 否 | ✅ | ✅ |
七、核心理念总结
- 获取时间:
gettimeofday()—— 快、够精、用户态;- 定时任务:
timerfd_*—— 精度高、无信号干扰、易于与 epoll 集成;- Reactor 统一事件模型:IO 与定时都用“fd”驱动;
- 非实时系统中追求高可靠时间精度而非“绝对精确”。
