什么是 SIGVTALRM?
SIGVTALRM 是一个由 POSIX 标准定义的信号,它的全称是 "Virtual Timer Alarm"(虚拟定时器警报)。

(图片来源网络,侵删)
它的核心作用是:当一个进程的“虚拟运行时间”(virtual time)达到指定值时,内核会向该进程发送 SIGVTALRM 信号。
“虚拟运行时间” (Virtual Time) 是什么?
这是理解 SIGVTALRM 的关键,虚拟运行时间指的是进程在用户态(User Space)下实际执行 CPU 指令的时间。
它不包括:
- 进程在内核态执行系统调用的时间。
- 进程因为等待 I/O(如读写文件、网络通信)而被阻塞的时间。
- 进程因为等待其他事件(如等待锁)而被调度器挂起的时间。
与之相对的是实际运行时间(Real Time),即从时钟开始到现在经过的绝对时间,由 real() 函数返回。

(图片来源网络,侵删)
简单比喻: 想象一个工人(进程):
- 虚拟时间:工人真正在干活的时间。
- 实际时间:工人从上班到下班的总时长,包括他干活的时间、喝水休息的时间、和同事聊天的时间、上厕所的时间等。
SIGVTALRM 只关心“工人真正在干活”的时间。
如何使用 SIGVTALRM?
要使用 SIGVTALRM,你需要用到 <sys/time.h> 头文件中的两个函数:
setitimer(): 用来设置一个定时器。signal()或sigaction(): 用来定义信号处理函数,即当SIGVTALRM发生时,程序应该做什么。
setitimer() 函数详解
setitimer() 是设置定时器的核心函数。
#include <sys/time.h> int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
which: 指定要设置哪个定时器,对于SIGVTALRM,这个值必须是ITIMER_VIRTUAL。new_value: 一个指向itimerval结构体的指针,包含了你希望设置的定时器参数。old_value: 一个指向itimerval结构体的指针,用于保存旧的定时器设置,如果不需要,可以传NULL。
itimerval 结构体
这个结构体定义了定时器的工作方式:
struct itimerval {
struct timeval it_interval; // 定时器的间隔时间
struct timeval it_value; // 定时器的初始超时时间
};
struct timeval {
long tv_sec; // 秒
long tv_usec; // 微秒 (1秒 = 1,000,000微秒)
};
itimerval 中的两个 timeval 结构体决定了定时器的两种行为模式:
-
单次触发模式:
- 只设置
it_value,不设置it_interval(或者将其设为 0)。 - 当
it_value指定的时间耗尽后,定时器触发一次SIGVTALRM信号,然后停止工作。
- 只设置
-
周期性触发模式:
- 同时设置
it_value和it_interval。 - 当
it_value指定的时间耗尽后,定时器触发SIGVTALRM信号。 - 定时器会自动重置为
it_interval指定的时间,并开始下一次倒计时,如此循环往复。
- 同时设置
完整代码示例
下面是一个完整的例子,演示了如何使用 SIGVTALRM 实现一个简单的性能剖析工具,计算一段代码在用户态下运行了多长时间。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/time.h>
#include <signal.h>
#include <string.h>
// 全局变量,用于存储用户态运行的总时间
volatile unsigned long user_time_used = 0;
// 信号处理函数
void signal_handler(int signo) {
if (signo == SIGVTALRM) {
printf("Caught SIGVTALRM! This means we've used some CPU time.\n");
// 每次收到信号,表示已经过去了 it_interval 的时间
user_time_used += 100000; // 假设我们设置的间隔是 0.1 秒
}
}
// 模拟一个耗时的计算任务(纯用户态)
void cpu_intensive_task() {
volatile unsigned long sum = 0;
for (int i = 0; i < 100000000; ++i) {
sum += i;
}
printf("CPU intensive task finished. Sum = %lu\n", sum);
}
int main() {
struct itimerval new_value, old_value;
// 1. 注册信号处理函数
if (signal(SIGVTALRM, signal_handler) == SIG_ERR) {
perror("signal");
exit(EXIT_FAILURE);
}
// 2. 设置定时器
// 设置初始超时时间为 0.1 秒
new_value.it_value.tv_sec = 0;
new_value.it_value.tv_usec = 100000; // 0.1 seconds
// 设置间隔时间为 0.1 秒,实现周期性触发
new_value.it_interval.tv_sec = 0;
new_value.it_interval.tv_usec = 100000;
// 设置 ITIMER_VIRTUAL 定时器
if (setitimer(ITIMER_VIRTUAL, &new_value, &old_value) == -1) {
perror("setitimer");
exit(EXIT_FAILURE);
}
printf("Timer set. Starting CPU intensive task...\n");
printf("The timer will trigger every 0.1 seconds of CPU time.\n");
// 3. 执行一个纯用户态的任务
// 在这个任务执行期间,定时器会开始倒计时
cpu_intensive_task();
// 4. 停止定时器
// 将 it_value 和 it_interval 都设为 0,即可停止定时器
new_value.it_value.tv_sec = 0;
new_value.it_value.tv_usec = 0;
new_value.it_interval.tv_sec = 0;
new_value.it_interval.tv_usec = 0;
setitimer(ITIMER_VIRTUAL, &new_value, NULL);
printf("\nTask finished.\n");
printf("Total user CPU time used (approximate): %lu microseconds\n", user_time_used);
return 0;
}
代码解释:
signal_handler: 当SIGVTALRM被触发时,这个函数会被调用,我们在这里只是简单地打印一条信息,并累加一个计数器。cpu_intensive_task: 这个函数只包含一个循环,没有 I/O 操作或系统调用,所以它消耗的时间完全是用户态 CPU 时间。main函数:- 首先注册
SIGVTALRM的处理函数。 - 然后调用
setitimer(),设置一个ITIMER_VIRTUAL定时器,让它每 0.1 秒(10万微秒)触发一次。 - 接着调用
cpu_intensive_task(),在函数执行期间,你会看到每隔一段时间,signal_handler就会被调用一次。 - 任务结束后,我们停止定时器。
- 最后打印出累计的用户态时间。
- 首先注册
SIGVTALRM vs. SIGALRM
这是一个非常常见的对比点,两者都是定时器信号,但用途不同。
| 特性 | SIGVTALRM (Virtual Timer Alarm) |
SIGALRM (Real Timer Alarm) |
|---|---|---|
| 触发依据 | 虚拟时间 (用户态 CPU 时间) | 实际时间 (Wall-Clock Time) |
| 包含时间 | 不包括内核态和阻塞时间 | 包括所有时间(用户态、内核态、阻塞) |
| 定时器类型 | setitimer(ITIMER_VIRTUAL, ...) |
setitimer(ITIMER_REAL, ...) 或 alarm() |
| 典型用途 | 性能剖析、CPU 时间限制、资源监控 | 实时任务调度、超时控制、程序总运行时间限制 |
简单总结:
- 如果你关心的是程序真正干活花了多少 CPU 时间,用
SIGVTALRM。 - 如果你关心的是从开始到现在过去了多久,用
SIGALRM。
注意事项
- 信号处理函数要尽量简单:信号处理函数中应避免调用不可重入的函数(如
printf,malloc,exit等),因为信号可能在任何时候打断主程序,在上面的例子中,为了演示方便使用了printf,但在生产代码中应格外小心。 - 信号可能丢失:如果信号处理函数正在执行时,又发生了同类型的信号,这个新的信号可能会被“丢失”(在较旧的系统上)或被排队(在新的系统上,取决于
sigaction的设置)。 - 多线程:
SIGVTALRM信号是发送给整个进程的,如果一个多线程程序设置了SIGVTALRM,那么当定时器到期时,信号会被发送到进程中一个任意的、未被阻塞该信号的线程,在多线程环境中使用SIGVTALRM需要特别小心,通常推荐使用pthread提供的更精细的定时机制。
