37、信号与时间管理:Linux 编程中的关键概念

信号与时间管理: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 编程中更加得心应手。在实际开发中,要根据具体的需求和场景,灵活运用这些知识,提高程序的性能和稳定性。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值