目录

操作系统-之-时钟子系统

操作系统 之 ③ 时钟子系统

概述

时钟子系统 是操作系统中至关重要的一部分,负责管理系统的时间和定时功能。它的主要职责包括:

  • 时间管理: 提供系统时间的获取和管理,确保操作系统和应用程序能够正确获取当前时间。
  • 任务调度: 通过定时器和时间片控制多任务的执行,确保每个任务都能在合理的时间内运行(在任务调度章节里时我们可以直到任务的调度和系统时间是紧密联系在一起的),提高系统的响应速度和资源利用率。
  • 时钟中断: 定期产生中断信号,通知内核执行调度和其他时间相关的操作。
  • 系统心跳: 维持系统的正常运行,保证各个子系统之间的同步
  • 高精度计时: 支持高精度的定时器可以满足时间敏感的应用需求,在实时操作系统、视频音乐的播放等等中需求高

核心模块

1> 时钟中断

概述

时钟中断 是操作系统中调度机制的基础,它是一种由硬件定时器定期产生的中断信号,提供操作系统执行一系列时间相关的任务和操作。他是操作系统用于管理任务调度、系统时间和定时器服务的关键机制。它确保系统能够实时更新状态、精确控制任务的执行顺序,并维持系统的正常运行。通过定时产生中断信号,操作系统能有效管理多任务环境中的 CPU 资源,实现高效的调度和响应。

详细解释

1> 来源: 时钟中断通常是由硬件定时器(如 Programmable Interval Timer、PIT)或更现代的计时器(如 APIC、HPET)产生。定时器被配置为在特定时间间隔产生中断信号,这个间隔可以是几毫秒到几微秒不等。

2> 触发机制:

  • 硬件计时器 会按照预设的时间间隔计数,当计数器达到设定值时,会向 CPU 发送一个中断信号。这个信号会中断当前正在执行的进程。
  • CPU 收到中断信号后,立即暂停当前正在执行的任务,保存任务的上下文信息(如寄存器状态、程序计数器等等)。
  • 接着 CPU 跳转到内核的中断处理程序,执行预定义的时钟中断处理任务(更新系统时间、任务调度、处理定时器事件、统计和监护、维护系统心跳等等) https://i-blog.csdnimg.cn/direct/cea87c48aff346679abfd0799cbf3668.png

3> 作用:

  • 任务调度: 时钟中断触发调度器,操作系统会检查当前任务的时间片是否用尽,如果用尽,操作系统会切换到下一个任务。通过系统中断,系统能够公平地分配 CPU 时间给所有任务。
  • 维护系统时间: 每次发生时钟中断时,操作系统会更新系统地时间变量,从而保证系统能够正确地记录和管理当前的时间。这也是实现系统调用如 gettimeofday() 等功能的基础。
  • 定时器服务: 时钟中断可以用于处理用户设定的定时任务(定时器超时任务等等)。当用户设定了一个定时任务,系统通过时钟中断来检测定时任务是否到期,并执行相关操作。
  • 系统心跳: 时钟中断用于产生系统心跳信号,维持系统的正常运行。定期的心跳中断确保各个系统组件保持同步。

在 Linux 中的中断处理程序代码实现:

// 时钟中断处理程序,当定时器硬件触发中断时,该函数会被调用
irqreturn_t timer_interrupt(int irq, void *dev_id) {
    // 更新系统时间
    update_system_time();
    
    // 调用调度器,判断是否需要切换任务
    scheduler_tick();

    // 检查是否有定时器到期
    check_timers();

    return IRQ_HANDLED;
}

2> 定时器

概述

定时器 是操作系统中用于在指定的未来时间点触发莫格时间或操作的机制。定时器应用于包括任务的延时执行、周期性任务、事件计时、系统超时检测等。操作系统内核中的定时器主要依赖时钟中断来实现,时钟中断定期出发后,操作系统会检查是否有定时器到期。

核心任务:

  1. 设置定时器的到期时间。
  2. 检测定时器是否到期并执行指定的回调函数。
  3. 支持多种定时器模式(如单次触发或周期触发)

主要内容

1> 定时器结构:

  • 到期时间: 定时器在未来触发的具体时间点。
  • 回调函数: 定时器到期时要执行的函数。
  • 定时器模式: 定时器是单次执行还是周期性执行。

下面是简化的定时器结构体定义:

typedef struct {
    unsigned long long expires; // 定时器到期时间(相对于系统启动时间)
    void (*function)(void *);   // 到期是执行的回调函数
    void *data;                 // 回调函数的参数
    int periodic;               // 定时器模式,单次执行(0) or 周期执行(1)
} Timer;

2> 定时器检查与处理: 时钟中断每次触发时,操作系统会检查定时器集列表,找出那些已经到期的定时器并执行回调函数。

void check_timers(void) {
    unsigned long long currTime = get_system_time();

    // 遍历定时器列表,检查是否有到期的定时器
    //定时列表也可以抽象成一个数组,遍历数组即可
    struct Timer *timer;
    for_timer_in_timeList(timer) {
        if (timer->expires <= currTime) { // 已过期
            // 调用定时器的回调函数
            timer->function(timer->data);

            // 如果是周期性定时器,重新设置下次的触发时间
            if (timer->periodic) {
                timer->expires += PERIODIC_INTERVAL;
            } else { // 如果是单次定时器,就直接删除
                remove_timer(timer);
            }
        }
    }
    return ;
}

3> 用户态接口: 操作系统提供给用户程序的接口通常包括设置定时器,取消定时器等系统调用。例如,在 Linux 中,settimer() 和 alarm() 是常用的定时器系统调用。

// 设置用户态定时器
int sys_setitimer(int which, struct itimerval *new_value, struct itimerval *old_value)
{
    // 将定时器信息传递到内核中进行处理
    struct timer_list user_timer;
    user_timer.expires = new_value->it_value.tv_sec * 1000 + new_value->it_value.tv_usec / 1000;
    user_timer.function = user_defined_callback;

    // 添加到内核定时器列表
    add_timer(&user_timer, user_timer.expires);

    return 0;
}

3> 系统时间管理

概述

系统时间管理 是操作系统中用于维护、获取和操作系统时间的机制。它确保系统能够跟踪当前时间、处理与时间相关的事件,并提供接口供应程序使用。系统时间管理的核心功能包括时间的 获取、更新、格式化 以及相关的系统调用。

主要内容

1> 系统时间的概念: 系统时间通常是操作系统维护,表示系统启动以来经过的时间。它可以用不同的时间单位表示,如:

  • 秒(s): 一般用于表示比较大的时间间隔。
  • 毫秒(ms): 常用于定时器和高频任务。
  • 微妙(us): 用于高精度及时的需求。

2> 系统时间的维护: 操作系统通过时钟中断来维护系统时间。每当时钟中断发生时,操作系统会更细系统时间。一般来说,它会维护一个全局的时间变量,代表当前的系统时间。下面是简化的系统时间维护的代码

unsigned long system_time; // 系统时间变量

void update_system_time() {
    system_time += TICK_INTERVAL; // TICK_INTERVAL为时钟中断的时间间隔
}

3> 系统调用接口: 这些接口可以让用户程序能够获取或设置系统时间。这些系统调用包括:

  • gettimeofday() : 获取当前时间,返回自 1970 年 1 月 1日 以来的秒和微秒数。
  • clock_gettime() : 获取高精度时间,支持多种时钟(如实时时钟、系统时钟等)
  • settimeofday() : 设置系统时间。

在 Linux 中的实现:

// tv 用于获取目标结构体中的数据
void do_gettimeofday(struct timeval *tv) {
    struct timespec ts; // 定义一个 timespec 结构体,用于存储高精度时间(秒和纳秒)
    
    ktime_get_real_ts64(&ts); // 获取当前真实时间(UTC),并存储在 ts 中
    
    // 将 timespec 转换为 timeval,存储秒数和微秒数
    tv->tv_sec = ts.tv_sec; // 获取秒部分
    tv->tv_usec = ts.tv_nsec / 1000; // 将纳秒转换为微秒并存储
}

// clk_id 用于指定请求的时钟类型,ts 用于获取目标结构体中的数据
int clock_gettime(clockid_t clk_id, struct timespec *ts) {
    switch (clk_id) {
        case CLOCK_REALTIME: // 如果请求的是实时钟
            return do_gettimeofday(ts); // 调用 do_gettimeofday 函数获取时间
        // 其他时钟类型的处理(如 CLOCK_MONOTONIC)可以在这里添加
        
        default:
            return -EINVAL; // 如果传入的时钟 ID 无效,返回错误
    }
}

// tv 用于插入数据中的结构体,tz 用于指定时区信息
int settimeofday(const struct timeval *tv, const struct timezone *tz) {
    // 更新系统时间的实现
    // 处理时区信息的代码可以在这里实现
    
    return 0; // 返回 0 表示成功
}

4> 时间同步: 主要是为了确保系统时间与外部时间源(如网络时间协议 NTP 服务器)保持一致。这对于分布式系统和需要精确时间的应用非常重要。例如 NTP(通过互联网同步时间)、PTP(用于高精度时间同步)。

其他话题

时钟分辨率与调度

1> 时钟分辨率的定义:时钟分辨率 是指系统时钟能够测量和响应时间变化的最小单位,通常以毫秒(ms)、微秒(us)、纳秒(ns)表示。高分辨率时钟可以以更小的时间间隔进行事件调度,使用于需要精确控制时间的应用场景。

2> 时钟频率与分辨率:时钟频率 是指系统时钟每秒钟产生的中断次数。假设一个系统的时钟频率为 1000Hz(每秒 1000 次中断),则时钟分辨率在 1ms(1 秒/ 1000 次)。频率越高,分辨率越小,系统越能够更精确地处理时间相关任务。

3> 调度粒度:调度粒度 是指操作系统在进行任务调度是所能实现的最小时间片。较小的时间片会导致系统更加频繁地切换任务,有助于提高响应性,但是会增加上下文切换开销。

4>时钟分辨率对调度的影响

  • 响应时间: 较高的时钟分辨率能够更快地响应事件,从而减少任务地响应事件。例如实时操作系统通常需要较高地时钟分辨率,以确保外部事件(如键盘输入)地快速响应。
  • 任务切换: 在多任务环境中,调度器依赖时钟中断来决定何时切换任务。高分辨率时钟允许更精确地时间片控制,从而更有效地管理任务之间的切换。
  • CPU 利用率: 过大的时间片可能导致 CPU 空闲时间增加,因为某些任务可能未能在分配的时间内完成,导致 CPU 等待下一次调度,这会降低系统的总体利用率。相反,过小的时间片会增加上下问切换的频率,从而引入额外的开销,降低 CPU 效率。
  • 如何选择合适的时间片: <1> 任务特性: 实时任务通常需要较小的时间片,以确保及时响应。非实时任务可以使用更大的时间片以减少上下文切换的开销。 <2> 系统负载: 在高负载情况下,较小的时间片可能有助于提高响应性,但在低负载情况下则可能导致不必要的上下文切换。 <2> 优先级策略: 实时任务通常使用较小的时间片,而低优先级任务则可以使用较大的时间片。

5> 总结: 时钟分辨率与调度之间的关系至关重要。高时钟分辨率能够提供更精确的时间控制,改善系统的响应能力和任务切换效率。然而,在选择时钟频率和时间片大小时,需要综合考虑系统负载、任务特性和实时需求,以确保系统高效运行。合理的调度策略可以提高 CPU 利用率,降低延迟,并确保任务的及时响应。