信号与时间管理:Linux 编程中的关键概念
1. 带有效负载发送信号
在 Linux 系统中,信号是一种重要的内核与用户通信机制。当使用
SA_SIGINFO
标志注册信号处理程序时,会传递一个
siginfo_t
参数,其中包含
si_value
字段,这是一个从信号发送者传递到接收者的可选有效负载。
POSIX 定义了
sigqueue()
函数,允许进程发送带有有效负载的信号,其原型如下:
#include <signal.h>
int sigqueue (pid_t pid,
int signo,
const union sigval value);
sigqueue()
的工作方式与
kill()
类似。成功时,由
signo
标识的信号会被排队到由
pid
标识的进程或进程组,函数返回 0。信号的有效负载由
value
给出,它是一个整数和
void
指针的联合:
union sigval {
int sival_int;
void *sival_ptr;
};
调用失败时,函数返回 -1,并将
errno
设置为以下值之一:
| 错误码 | 含义 |
| ---- | ---- |
|
EAGAIN
| 调用进程已达到排队信号的限制 |
|
EINVAL
|
signo
指定的信号无效 |
|
EPERM
| 调用进程没有足够的权限向任何请求的进程发送信号 |
|
ESRCH
|
pid
表示的进程或进程组不存在,或者进程是僵尸进程 |
与
kill()
一样,可以传递空信号(0)来测试权限。
以下是一个发送带有有效负载信号的示例,向进程 ID 为 1722 的进程发送
SIGUSR2
信号,有效负载为整数 404:
sigval value;
int ret;
value.sival_int = 404;
ret = sigqueue (1722, SIGUSR2, value);
if (ret)
perror ("sigqueue");
如果进程 1722 使用
SA_SIGINFO
处理程序处理
SIGUSR2
,它将发现
signo
设置为
SIGUSR2
,
si->si_int
设置为 404,
si->si_code
设置为
SI_QUEUE
。
2. Unix 信号的缺陷
信号在许多 Unix 程序员中声誉不佳。它们是一种古老、过时的内核与用户通信机制,充其量只是一种原始的进程间通信形式。在多线程程序和事件循环的世界中,信号显得格格不入。
然而,我们仍然离不开信号。它们是从内核接收许多通知(如非法操作码执行通知)的唯一方式,也是 Unix(包括 Linux)终止进程和管理父子关系的方式。因此,程序员必须理解并使用它们。
信号不受欢迎的主要原因之一是编写一个安全的、可重入的信号处理程序很困难。如果保持处理程序简单,并只使用特定列表中的函数,应该是安全的。另一个问题是,许多程序员仍然使用
signal()
和
kill()
进行信号管理,而不是更强大的
sigaction()
和
sigqueue()
。使用
SA_SIGINFO
风格的信号处理程序时,信号会更强大和灵活。
3. 时间的测量方式
在现代操作系统中,时间有多种用途,许多程序需要跟踪时间。内核以三种不同的方式测量时间:
-
挂钟时间(或实时时间)
:这是现实世界中的实际时间和日期,即墙上时钟显示的时间。进程在与用户交互或为事件添加时间戳时使用挂钟时间。
-
进程时间
:这是进程在处理器上执行所花费的时间,可分为进程自身执行的时间(用户时间)和内核为进程工作的时间(系统时间)。进程关心进程时间用于性能分析、审计和统计等目的。由于 Linux 的多任务性质,挂钟时间通常大于进程时间;而在多处理器和多线程进程的情况下,进程时间可能超过挂钟时间。
-
单调时间
:这是一个严格线性增加的时间源,大多数操作系统(包括 Linux)使用系统启动时间来表示。挂钟时间可能会改变,例如用户设置或系统调整以纠正时钟偏差,而系统启动时间是一个确定性且不可改变的时间表示。单调时间的重要之处不在于当前值,而在于它保证严格线性增加,可用于计算两次采样之间的时间差。
这三种时间测量方式可以用两种格式表示:
-
相对时间
:相对于某个基准的值,如从现在起 5 秒或 10 分钟前。单调时间适用于计算相对时间。
-
绝对时间
:不依赖于任何基准的时间表示,如 1968 年 3 月 25 日中午。挂钟时间适合计算绝对时间。
4. 时间的表示格式
Unix 系统将绝对时间表示为自纪元(1970 年 1 月 1 日 00:00:00 UTC)以来经过的秒数。UTC 大致相当于 GMT 或祖鲁时间。有趣的是,在 Unix 中,即使是绝对时间在底层也是相对的。
操作系统通过软件时钟跟踪时间的流逝,内核实例化一个周期性定时器(系统定时器),以特定频率触发。定时器间隔结束时,内核将经过的时间增加一个单位,称为滴答或 jiffy。经过的滴答数计数器称为 jiffies 计数器。从 Linux 2.6 内核开始,jiffies 是一个 64 位计数器。
在 Linux 中,系统定时器的频率称为 HZ,其值是特定于架构的,不是 Linux ABI 的一部分,因此程序不能依赖或期望特定的值。历史上,x86 架构使用的值为 100,即系统定时器每秒运行 100 次,每个 jiffy 为 0.01 秒。在 2.6 内核中,HZ 提高到 1000,每个 jiffy 为 0.001 秒;在 2.6.13 及以后的版本中,HZ 为 250,每个 jiffy 为 0.004 秒。HZ 值存在权衡:较高的值提供更高的分辨率,但会带来更大的定时器开销。
虽然进程不应依赖于固定的 HZ 值,但 POSIX 定义了一种在运行时确定系统定时器频率的机制:
long hz;
hz = sysconf (_SC_CLK_TCK);
if (hz == −1)
perror ("sysconf"); /* should never occur */
这个接口在程序想要确定系统定时器的分辨率时很有用,但在将系统时间值转换为秒时通常不需要,因为大多数 POSIX 接口导出的时间测量值已经转换或按固定频率缩放,与 HZ 无关。这个固定频率是系统 ABI 的一部分,在 x86 上值为 100。POSIX 函数以时钟滴答表示时间时,使用
CLOCKS_PER_SEC
表示固定频率。
大多数计算机都有一个电池供电的硬件时钟,用于在计算机关机时存储时间和日期。内核启动时,从硬件时钟初始化当前时间的概念;用户关闭系统时,内核将当前时间写回硬件时钟。系统管理员可以通过
hwclock
命令在其他时间同步时间。
5. 时间的数据结构
随着 Unix 系统的发展,出现了多种数据结构来表示时间:
-
time_t
:定义在
<time.h>
头文件中,通常是
C
语言
long
类型的简单 typedef,表示自纪元以来经过的秒数。32 位
long
类型的
time_t
可以表示到 2038 年 1 月 18 日星期一 22:14:07,之后可能会溢出。
-
timeval
:定义在
<sys/time.h>
头文件中,扩展了
time_t
以增加微秒精度:
#include <sys/time.h>
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
-
timespec:定义在<time.h>头文件中,提供纳秒精度:
#include <time.h>
struct timespec {
time_t tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
-
tm:定义在<time.h>头文件中,用于以更易读的格式表示“分解”时间:
#include <time.h>
struct tm {
int tm_sec; /* seconds */
int tm_min; /* minutes */
int tm_hour; /* hours */
int tm_mday; /* the day of the month */
int tm_mon; /* the month */
int tm_year; /* the year */
int tm_wday; /* the day of the week */
int tm_yday; /* the day in the year */
int tm_isdst; /* daylight savings time? */
#ifdef _BSD_SOURCE
long tm_gmtoff; /* time zone's offset from GMT */
const char *tm_zone; /* time zone abbreviation */
#endif /* _BSD_SOURCE */
};
tm
结构的各个字段含义如下:
| 字段 | 含义 | 范围 |
| ---- | ---- | ---- |
|
tm_sec
| 分钟后的秒数,通常为 0 - 59,可高达 61 表示最多两个闰秒 | 0 - 61 |
|
tm_min
| 小时后的分钟数 | 0 - 59 |
|
tm_hour
| 午夜后的小时数 | 0 - 23 |
|
tm_mday
| 月份中的日期,POSIX 未指定 0,Linux 用它表示前一个月的最后一天 | 0 - 31 |
|
tm_mon
| 自 1 月以来的月数 | 0 - 11 |
|
tm_year
| 自 1900 年以来的年数 | |
|
tm_wday
| 自星期日以来的天数 | 0 - 6 |
|
tm_yday
| 自 1 月 1 日以来的天数 | 0 - 365 |
|
tm_isdst
| 指示夏令时是否生效,正数表示生效,0 表示未生效,负数表示未知 | |
|
tm_gmtoff
| 当前时区相对于格林威治标准时间的偏移秒数,仅在定义
_BSD_SOURCE
时存在 | |
|
tm_zone
| 当前时区的缩写,仅在定义
_BSD_SOURCE
时存在 | |
-
clock_t:表示时钟滴答,是一个整数类型,通常是long。根据接口的不同,clock_t表示的滴答可以是系统的实际定时器频率(HZ)或CLOCKS_PER_SEC。
6. POSIX 时钟
本章讨论的一些系统调用使用 POSIX 时钟,这是一种实现和表示时间源的标准。
clockid_t
类型表示特定的 POSIX 时钟,Linux 支持五种:
| 时钟类型 | 含义 |
| ---- | ---- |
|
CLOCK_REALTIME
| 系统范围的实时(挂钟时间)时钟,设置此时钟需要特殊权限 |
|
CLOCK_MONOTONIC
| 单调递增的时钟,任何进程都无法设置,代表自某个未指定起点(如系统启动)以来的经过时间 |
|
CLOCK_MONOTONIC_RAW
| 类似于
CLOCK_MONOTONIC
,但时钟不进行时钟偏差校正,是 Linux 特有的 |
|
CLOCK_PROCESS_CPUTIME_ID
| 处理器提供的高分辨率、每个进程的时钟,例如在 x86 架构上使用时间戳计数器(TSC)寄存器 |
|
CLOCK_THREAD_CPUTIME_ID
| 类似于每个进程的时钟,但每个线程独有 |
POSIX 只要求
CLOCK_REALTIME
,因此,虽然 Linux 可靠地提供了所有五种时钟,但可移植代码应仅依赖于
CLOCK_REALTIME
。
7. 时间源分辨率
POSIX 定义了
clock_getres()
函数来获取给定时间源的分辨率:
#include <time.h>
int clock_getres (clockid_t clock_id,
struct timespec *res);
成功调用
clock_getres()
时,如果
res
不为 NULL,则将
clock_id
指定的时钟分辨率存储在
res
中,并返回 0。失败时,函数返回 -1,并将
errno
设置为以下错误代码之一:
| 错误码 | 含义 |
| ---- | ---- |
|
EFAULT
|
res
是无效指针 |
|
EINVAL
|
clock_id
不是此系统上的有效时间源 |
以下是一个输出五种时间源分辨率的示例:
clockid_t clocks[] = {
CLOCK_REALTIME,
CLOCK_MONOTONIC,
CLOCK_PROCESS_CPUTIME_ID,
CLOCK_THREAD_CPUTIME_ID,
CLOCK_MONOTONIC_RAW,
(clockid_t) −1 };
int i;
for (i = 0; clocks[i] != (clockid_t) −1; i++) {
struct timespec res;
int ret;
ret = clock_getres (clocks[i], &res);
if (ret)
perror ("clock_getres");
else
printf ("clock=%d sec=%ld nsec=%ld\n",
clocks[i], res.tv_sec, res.tv_nsec);
}
在现代 x86 系统上,输出类似于:
clock=0 sec=0 nsec=4000250
clock=1 sec=0 nsec=4000250
clock=2 sec=0 nsec=1
clock=3 sec=0 nsec=1
clock=4 sec=0 nsec=4000250
4,000,250 纳秒是 4 毫秒,即 0.004 秒,这是 HZ 值为 250 时 x86 系统时钟的分辨率。可以看到,
CLOCK_REALTIME
和
CLOCK_MONOTONIC
与 jiffies 和系统定时器提供的分辨率相关;而
CLOCK_PROCESS_CPUTIME_ID
和
CLOCK_THREAD_CPUTIME_ID
使用更高分辨率的时间源,在 x86 机器上是 TSC,提供纳秒分辨率。
在 Linux(和大多数其他 Unix 系统)上,所有使用 POSIX 时钟的函数都需要将生成的目标文件与
librt
链接。例如,编译上述代码片段为完整的可执行文件,可以使用以下命令:
$ gcc -Wall -W -O2 -lrt -g -o snippet snippet.c
8. 获取当前时间
应用程序有多种原因需要获取当前时间和日期,如向用户显示、计算相对或经过时间、为事件添加时间戳等。最简单且历史上最常用的获取当前时间的方法是
time()
函数:
#include <time.h>
time_t time (time_t *t);
信号与时间管理:Linux 编程中的关键概念
9. 时间管理的操作与应用
在 Unix 系统中,管理时间的流逝涉及多个任务,不同进程可能只关注其中一部分,这些任务包括设置和获取当前挂钟时间、计算经过时间、休眠指定时间、进行高精度时间测量以及控制定时器等。下面我们详细介绍这些操作及相关应用。
9.1 设置和获取当前挂钟时间
-
获取时间
:除了前面提到的
time()函数,还可以使用gettimeofday()函数获取更精确的当前时间,它返回的时间包含秒和微秒信息。
#include <sys/time.h>
int gettimeofday(struct timeval *tv, struct timezone *tz);
tv
是一个指向
timeval
结构体的指针,用于存储获取到的时间;
tz
通常设置为
NULL
。
-
设置时间
:可以使用
settimeofday()
函数设置系统的当前时间,该函数需要特殊权限。
#include <sys/time.h>
int settimeofday(const struct timeval *tv, const struct timezone *tz);
9.2 计算经过时间
计算经过时间通常需要记录两个时间点,然后计算它们之间的差值。可以使用
timeval
或
timespec
结构体来实现。
#include <sys/time.h>
#include <stdio.h>
void calculate_elapsed_time(struct timeval start, struct timeval end) {
long seconds = end.tv_sec - start.tv_sec;
long microseconds = end.tv_usec - start.tv_usec;
if (microseconds < 0) {
seconds--;
microseconds += 1000000;
}
printf("Elapsed time: %ld seconds, %ld microseconds\n", seconds, microseconds);
}
int main() {
struct timeval start, end;
gettimeofday(&start, NULL);
// 模拟一些操作
for (int i = 0; i < 1000000; i++) {}
gettimeofday(&end, NULL);
calculate_elapsed_time(start, end);
return 0;
}
9.3 休眠指定时间
可以使用
sleep()
函数让进程休眠指定的秒数,或者使用
usleep()
函数让进程休眠指定的微秒数。
#include <unistd.h>
// 休眠 5 秒
sleep(5);
// 休眠 1000 微秒
usleep(1000);
另外,
nanosleep()
函数可以提供更高精度的休眠,以纳秒为单位。
#include <time.h>
int nanosleep(const struct timespec *req, struct timespec *rem);
req
是一个指向
timespec
结构体的指针,指定要休眠的时间;
rem
用于存储未休眠完的时间,如果休眠被信号中断。
9.4 高精度时间测量
对于需要高精度时间测量的场景,可以使用
clock_gettime()
函数。
#include <time.h>
int clock_gettime(clockid_t clk_id, struct timespec *tp);
clk_id
可以是前面提到的 POSIX 时钟类型之一,
tp
用于存储获取到的时间。
9.5 控制定时器
可以使用
timer_create()
、
timer_settime()
和
timer_delete()
函数来创建、设置和删除定时器。
#include <time.h>
int timer_create(clockid_t clockid, struct sigevent *sevp, timer_t *timerid);
int timer_settime(timer_t timerid, int flags, const struct itimerspec *new_value, struct itimerspec *old_value);
int timer_delete(timer_t timerid);
以下是一个简单的定时器示例:
#include <stdio.h>
#include <signal.h>
#include <time.h>
void timer_handler(int signum) {
printf("Timer expired!\n");
}
int main() {
struct sigevent sev;
timer_t timerid;
struct itimerspec its;
// 注册信号处理函数
signal(SIGRTMIN, timer_handler);
// 创建定时器
sev.sigev_notify = SIGEV_SIGNAL;
sev.sigev_signo = SIGRTMIN;
timer_create(CLOCK_REALTIME, &sev, &timerid);
// 设置定时器
its.it_value.tv_sec = 2;
its.it_value.tv_nsec = 0;
its.it_interval.tv_sec = 0;
its.it_interval.tv_nsec = 0;
timer_settime(timerid, 0, &its, NULL);
// 等待定时器事件
pause();
// 删除定时器
timer_delete(timerid);
return 0;
}
10. 信号与时间管理的总结
信号和时间管理在 Linux 编程中是非常重要的概念。信号虽然存在一些缺陷,但仍然是内核与用户通信、进程管理的重要手段。在使用信号时,要注意编写安全的信号处理程序,尽量使用
sigaction()
和
sigqueue()
等更强大的函数。
时间管理方面,内核提供了多种时间测量方式和数据结构,不同的时间表示和测量方式适用于不同的场景。在进行时间管理操作时,要根据具体需求选择合适的函数和数据结构。
例如,在需要高精度时间测量时,可以使用
timespec
结构体和相关的高精度时间函数;在进行简单的时间获取时,
time_t
和
time()
函数就足够了。同时,要注意不同函数的权限要求和错误处理,确保程序的健壮性。
11. 流程图总结
下面是一个简单的 mermaid 流程图,总结了时间管理的主要操作流程:
graph LR
A[开始] --> B{选择操作}
B -->|获取时间| C[使用 time() 或 gettimeofday()]
B -->|设置时间| D[使用 settimeofday()]
B -->|计算经过时间| E[记录起始和结束时间并计算差值]
B -->|休眠| F[使用 sleep()、usleep() 或 nanosleep()]
B -->|高精度测量| G[使用 clock_gettime()]
B -->|控制定时器| H[使用 timer_create()、timer_settime() 和 timer_delete()]
C --> I[结束]
D --> I
E --> I
F --> I
G --> I
H --> I
通过以上对信号和时间管理的介绍,希望能帮助你更好地理解和应用这些概念,在 Linux 编程中更加得心应手。在实际开发中,要根据具体的需求和场景,灵活运用这些知识,提高程序的性能和稳定性。
超级会员免费看
4753

被折叠的 条评论
为什么被折叠?



