Skip to content

Latest commit

 

History

History
708 lines (542 loc) · 35.4 KB

File metadata and controls

708 lines (542 loc) · 35.4 KB

十、时钟和时间管理

Linux 时间管理子系统管理各种与时间相关的活动,并跟踪计时数据,如当前时间和日期、系统启动后经过的时间(系统正常运行时间)和超时,例如,等待特定事件启动或终止需要多长时间、超时后锁定系统或发出信号终止无响应的进程。

Linux 时间管理子系统处理两种类型的计时活动:

  • 保持当前时间和日期
  • 维护计时器

时间表示

根据用例,时间在 Linux 中以三种不同的方式表示:

  1. **墙时(或实时):**这是现实世界中的实际时间和日期,如 2017 年 8 月 10 日上午 07:00,用于通过网络发送的文件和数据包上的时间戳。
  2. **进程时间:**这是进程在其生命周期中消耗的时间。它包括进程在用户模式下消耗的时间和内核代码代表进程执行时消耗的时间。这对于统计、审计和分析非常有用。
  3. **单调时间:**这是系统启动后经过的时间。它本质上是不断增加和单调的(系统正常运行时间)。

这三个时间以下列任一方式测量:

  1. **相对时间:**这是相对于某个特定事件的时间,比如系统启动后的 7 分钟,或者用户上次输入后的 2 分钟。
  2. **绝对时间:**这是一个唯一的时间点,没有任何参考以前的事件,如 2017 年 8 月 12 日上午 10:00。在 Linux 中,绝对时间表示为自 1970 年 1 月 1 日午夜 00:00:00(世界协调时)以来经过的秒数

即使在重新启动和关闭之间,壁时间也在不断增加(除非用户修改过),但每次创建新流程或系统启动时,流程时间和系统正常运行时间都从某个预定义的时间点开始(通常为零)。

定时硬件

Linux 依靠合适的硬件设备来维持时间。这些硬件设备可以大致分为两类:系统时钟和定时器。

实时时钟

跟踪当前的时间和日期非常重要,这不仅是为了让用户知道它,也是为了将它用作系统中各种资源的时间戳,特别是辅助存储中的文件。每个文件都有元数据信息,如创建日期和上次修改日期,每次创建或修改文件时,这两个字段都会根据系统中的当前时间进行更新。几个应用使用这些字段来管理文件,例如对文件进行排序、分组甚至删除(如果文件很长时间没有被访问过)。 make 工具使用该时间戳来确定源文件自上次访问以来是否被编辑过;只有到那时,它才被编译,否则保持不变。

系统时钟实时时钟跟踪当前时间和日期;在额外电池的支持下,即使系统关闭,它也能继续工作。

RTC 可以定期在 IRQ8 上引发中断。该功能可用作报警功能,通过对 RTC 编程,当到达特定时间时,在 IRQ8 上产生中断。在 IBM 兼容的个人电脑中,实时时钟被映射到 0x70 和 0x71 输入/输出端口。可以通过/dev/rtc设备文件访问。

时间戳计数器

这是一个在每个 x86 微处理器中通过 64 位寄存器实现的计数器,称为 TSC 寄存器。它计算到达处理器 CLK 引脚的时钟信号数量。当前计数器值可以通过访问 TSC 寄存器来读取。每秒计数的滴答数可以计算为 1/(时钟频率);对于 1 千兆赫的时钟,它转换为每纳秒一次。

知道两个连续刻度之间的持续时间非常重要。一个处理器时钟的频率可能与其他处理器时钟的频率不同,这一事实使得它在不同的处理器之间有所不同。CPU 时钟频率在系统引导期间由arch/x86/include/asm/x86_init.h头文件中定义的 x86_platform_ops 结构的calibrate_tsc()回调例程计算:

struct x86_platform_ops {
        unsigned long (*calibrate_cpu)(void);
        unsigned long (*calibrate_tsc)(void);
        void (*get_wallclock)(struct timespec *ts);
        int (*set_wallclock)(const struct timespec *ts);
        void (*iommu_shutdown)(void);
        bool (*is_untracked_pat_range)(u64 start, u64 end);
        void (*nmi_init)(void);
        unsigned char (*get_nmi_reason)(void);
        void (*save_sched_clock_state)(void);
        void (*restore_sched_clock_state)(void);
        void (*apic_post_init)(void);
        struct x86_legacy_features legacy;
        void (*set_legacy_features)(void);
};

该数据结构还管理其他定时操作,例如通过get_wallclock()从实时时钟获取时间或通过set_wallclock()回调在实时时钟上设置时间。

可编程中断定时器

内核需要定期执行某些任务,例如:

  • 更新当前时间和日期(午夜)
  • 更新系统运行时间(正常运行时间)
  • 跟踪每个进程消耗的时间,这样它们就不会超过分配给在 CPU 上运行的时间
  • 跟踪各种计时器活动

为了执行这些任务,中断必须周期性地产生。每当这个周期性中断被引发时,内核就知道是时候更新前面提到的定时数据了。PIT 是负责发出这种周期性中断的硬件,称为定时器中断。PIT 以大约 1000 赫兹的频率定期在 IRQ0 上发出定时器中断,每毫秒一次。这种周期性的中断被称为滴答,发出的频率被称为滴答率。滴答频率由内核宏赫兹定义,单位为赫兹。

系统响应取决于滴答率:滴答越短,系统响应越快,反之亦然。滴答越短,poll()select()系统调用的响应时间越快。然而,更短的滴答率的相当大的缺点是,大部分时间中央处理器将在内核模式下工作(执行定时器中断的中断处理程序),留给用户模式代码(程序)在其上执行的时间更少。在高性能的中央处理器中,这不会是很大的开销,但是在较慢的中央处理器中,整体系统性能会受到很大影响。

为了在响应时间和系统性能之间达到平衡,大多数机器使用 100 赫兹的滴答速率。除了 Alpham68knomu使用 1000 Hz 的滴答速率外,其余的常见架构,包括 x86 (arm、powerpc、sparc、mips 等)都使用 100 Hz 的滴答速率。 x86 机器中常见的 PIT 硬件是英特尔 8253。它是通过地址 0x 40–0x 43 映射和访问的输入/输出。PIT 由setup_pit_timer()初始化,在arch/x86/kernel/i8253.c文件中定义:

void __init setup_pit_timer(void)
{
        clockevent_i8253_init(true);
        global_clock_event = &i8253_clockevent;
}

这在内部称为clockevent_i8253_init(),在<drivers/clocksource/i8253.c> 中定义:

void __init clockevent_i8253_init(bool oneshot)
{
        if (oneshot)
                i8253_clockevent.features |= CLOCK_EVT_FEAT_ONESHOT;
        /*
        * Start pit with the boot cpu mask. x86 might make it global
        * when it is used as broadcast device later.
        */
        i8253_clockevent.cpumask = cpumask_of(smp_processor_id());

        clockevents_config_and_register(&i8253_clockevent, PIT_TICK_RATE,
                                        0xF, 0x7FFF);
}
#endif

中央处理器本地定时器

PIT 是一个全局定时器,它引发的中断可以由 SMP 系统中的任何 CPU 处理。在某些情况下,拥有这样一个通用的计时器是有益的,而在其他情况下,每 CPU 计时器是更可取的。在 SMP 系统中,在每个 CPU 中保持进程时间并监控分配给进程的时间片,使用本地定时器会更加容易和高效。

本地 APIC 在最近的 x86 微处理器中嵌入了这样一个 CPU 本地定时器。中央处理器本地定时器可以发出中断一次或定期。它使用 32 位定时器,可以以非常低的频率发出中断(这个更宽的计数器允许在引发中断之前出现更多的滴答声)。APIC 计时器根据总线时钟信号工作。APIC 计时器与 PIT 非常相似,只是它位于中央处理器的本地,有一个 32 位计数器(PIT 有一个 16 位计数器),并使用总线时钟信号(PIT 使用自己的时钟信号)。

高精度事件计时器(HPET)

HPET 使用超过 10 兆赫的时钟信号,每 100 纳米秒发出一次中断,因此得名高精度。HPET 实现了一个 64 位主计数器,以如此高的频率计数。它是由英特尔和微软联合开发的,以满足新的高分辨率计时器的需求。HPET 嵌入了一组计时器。它们中的每一个都能够独立发出中断,并且可以由内核分配的特定应用使用。这些定时器作为定时器组进行管理,每个组最多可以有 32 个定时器。一个 HPET 最多可以实现 8 个这样的组。每个定时器都有一组比较器匹配寄存器T4。当匹配寄存器中的值与主计数器的值匹配时,定时器发出中断。定时器可以编程产生中断一次或定期。

寄存器是内存映射的,有可重定位的地址空间。在系统启动期间,基本输入输出系统设置寄存器的地址空间,并将其传递给内核。一旦基本输入输出系统映射了地址,它很少被内核重新映射。

ACPI 电源管理定时器(ACPI 光电倍增管)

ACPI 光电倍增管是一个简单的计数器,其固定频率时钟为 3.58 兆赫。它在每一个刻度上递增。PMT 是端口映射的;BIOS 在引导期间的硬件初始化阶段负责地址映射。PMT 比 TSC 更可靠,因为它以恒定的时钟频率工作。TSC 依赖于 CPU 时钟,根据当前负载,CPU 时钟可能会被欠锁或超频,导致时间膨胀和测量不准确。其中,HPET 是优选的,因为如果在系统中存在,它允许非常短的时间间隔。

硬件抽象

每个系统至少有一个时钟计数器。与机器中的任何硬件设备一样,这个计数器也由一个结构来表示和管理。硬件抽象由include/linux/clocksource.h头文件中定义的struct clocksource提供。该结构通过readenabledisablesuspendresume例程提供回调以访问和处理柜台上的电源管理:

struct clocksource {
        u64 (*read)(struct clocksource *cs);
        u64 mask;
        u32 mult;
        u32 shift;
        u64 max_idle_ns;
        u32 maxadj;
#ifdef CONFIG_ARCH_CLOCKSOURCE_DATA
        struct arch_clocksource_data archdata;
#endif
        u64 max_cycles;
        const char *name;
        struct list_head list;
        int rating;
        int (*enable)(struct clocksource *cs);
        void (*disable)(struct clocksource *cs);
        unsigned long flags;
        void (*suspend)(struct clocksource *cs);
        void (*resume)(struct clocksource *cs);
        void (*mark_unstable)(struct clocksource *cs);
        void (*tick_stable)(struct clocksource *cs);

        /* private: */
#ifdef CONFIG_CLOCKSOURCE_WATCHDOG
        /* Watchdog related data, used by the framework */
        struct list_head wd_list;
        u64 cs_last;
        u64 wd_last;
#endif
        struct module *owner;
};

成员multshift有助于获得相关单位的经过时间。

计算经过的时间

在此之前,我们知道在每个系统中都有一个自由运行的、不断递增的计数器,所有的时间都来自于它,无论是墙时间还是任何持续时间。计算时间(从计数器开始算起的秒数)的最自然的想法是将该计数器提供的周期数除以时钟频率,如下式所示:

时间(秒)=(计数器值)/(时钟频率)

然而,这种方法有一个缺点:它涉及除法(这是一种迭代算法,是四种基本算术运算中最慢的)和浮点计算,在某些架构上可能会慢一些。在嵌入式平台上工作时,浮点计算显然比在个人电脑或服务器平台上慢。

那么我们如何克服这个问题呢?不是除法,而是使用乘法和按位移位运算来计算时间。内核提供了一个助手例程,以这种方式导出时间。include/linux/clocksource.h中定义的clocksource_cyc2ns()将时钟源周期转换为纳秒:

static inline s64 clocksource_cyc2ns(u64 cycles, u32 mult, u32 shift)
{
        return ((u64) cycles * mult) >> shift;
}

这里,参数 cycles 是从时钟源经过的周期数,mult是周期到纳秒的乘数,shift是周期到纳秒的除数(2 的幂)。这两个参数都与时钟源有关。这些值由前面讨论的时钟源内核抽象提供。

时钟源硬件并不总是准确的;它们的频率可能会有所不同。这种时钟变化会导致时间漂移(使时钟运行得更快或更慢)。在这种情况下,可以调整变量 mult 来弥补这个时间漂移。

kernel/time/clocksource.c中定义的帮助程序clocks_calc_mult_shift(),帮助评估multshift因素:

void
clocks_calc_mult_shift(u32 *mult, u32 *shift, u32 from, u32 to, u32 maxsec)
{
        u64 tmp;
        u32 sft, sftacc= 32;

        /*
        * Calculate the shift factor which is limiting the conversion
        * range:
        */
        tmp = ((u64)maxsec * from) >> 32;
        while (tmp) {
                tmp >>=1;
                sftacc--;
        }

        /*
        * Find the conversion shift/mult pair which has the best
        * accuracy and fits the maxsec conversion range:
        */
        for (sft = 32; sft > 0; sft--) {
                tmp = (u64) to << sft;
                tmp += from / 2;
                do_div(tmp, from);
                if ((tmp >> sftacc) == 0)
                        break;
        }
        *mult = tmp;
        *shift = sft;
}

可以计算两个事件之间的持续时间,如下面的代码片段所示:

struct clocksource *cs = &curr_clocksource;
cycle_t start = cs->read(cs);
/* things to do */
cycle_t end = cs->read(cs);
cycle_t diff = end – start;
duration =  clocksource_cyc2ns(diff, cs->mult, cs->shift);

Linux 计时数据结构、宏和助手例程

我们现在将通过观察一些关键的计时结构、宏和帮助程序来扩展我们的意识,这些程序可以帮助程序员提取特定的时间相关数据。

jiffies变量保存系统启动后经过的节拍数。每发生一次滴答,瞬间递增 1。这是一个 32 位变量,意味着对于 100 赫兹的滴答率,溢出将在大约 497 天内发生(对于 1000 赫兹的滴答率,溢出将在 49 天内发生,17 小时内发生)。

*为了克服这个问题,使用了一个 64 位的变量jiffies_64,它允许在溢出发生前几千年到几百万年。jiffies变量相当于jiffies_64的 32 个最低有效位。同时拥有jiffiesjiffies_64变量的原因是,在 32 位机器中,64 位变量不能被原子访问;在处理这两个 32 位半部分时,需要一些同步,以避免任何计数器更新。/kernel/time/jiffies.c源文件中定义的函数get_jiffies_64()返回jiffies的当前值:

u64 get_jiffies_64(void)
{
        unsigned long seq;
        u64 ret;

        do {
                seq = read_seqbegin(&jiffies_lock);
                ret = jiffies_64;
        } while (read_seqretry(&jiffies_lock, seq));
        return ret;
}

在使用jiffies时,考虑回绕的可能性至关重要,因为在比较两个时间事件时,这会导致不可预测的结果。有四个宏可用于此目的,在include/linux/jiffies.h中定义:

#define time_after(a,b)           \
       (typecheck(unsigned long, a) && \
        typecheck(unsigned long, b) && \
        ((long)((b) - (a)) < 0))
#define time_before(a,b)       time_after(b,a)

#define time_after_eq(a,b)     \
       (typecheck(unsigned long, a) && \
        typecheck(unsigned long, b) && \
        ((long)((a) - (b)) >= 0))
#define time_before_eq(a,b)    time_after_eq(b,a)

所有这些宏都返回布尔值;参数 ab 是需要比较的时间事件。如果 a 恰好是 b 之后的时间,time_after()返回真,否则返回假。反之,如果 a 恰好在 b 之前,time_before()返回真,否则返回假。如果 a 和 b 相等,则time_after_eq()time_before_eq()都返回真。在include/linux/jiffies.h : 中,可以使用例程jiffies_to_msecs()jiffies_to_usecs()(在kernel/time/time.cjiffies_to_nsecs()中定义)将 Jiffies 转换为其他时间单位,如毫秒、微秒和纳秒

unsigned int jiffies_to_msecs(const unsigned long j)
{
#if HZ <= MSEC_PER_SEC && !(MSEC_PER_SEC % HZ)
        return (MSEC_PER_SEC / HZ) * j;
#elif HZ > MSEC_PER_SEC && !(HZ % MSEC_PER_SEC)
        return (j + (HZ / MSEC_PER_SEC) - 1)/(HZ / MSEC_PER_SEC);
#else
# if BITS_PER_LONG == 32
        return (HZ_TO_MSEC_MUL32 * j) >> HZ_TO_MSEC_SHR32;
# else
        return (j * HZ_TO_MSEC_NUM) / HZ_TO_MSEC_DEN;
# endif
#endif
}

unsigned int jiffies_to_usecs(const unsigned long j)
{
        /*
        * Hz doesn't go much further MSEC_PER_SEC.
        * jiffies_to_usecs() and usecs_to_jiffies() depend on that.
        */
        BUILD_BUG_ON(HZ > USEC_PER_SEC);

#if !(USEC_PER_SEC % HZ)
        return (USEC_PER_SEC / HZ) * j;
#else
# if BITS_PER_LONG == 32
        return (HZ_TO_USEC_MUL32 * j) >> HZ_TO_USEC_SHR32;
# else
        return (j * HZ_TO_USEC_NUM) / HZ_TO_USEC_DEN;
# endif
#endif
}

static inline u64 jiffies_to_nsecs(const unsigned long j)
{
        return (u64)jiffies_to_usecs(j) * NSEC_PER_USEC;
}

其他转换例程可以在include/linux/jiffies.h文件中探索。

时间值和时间类别

在 Linux 中,当前时间是通过保持自 1970 年 1 月 01 日午夜(称为纪元)以来经过的秒数来维护的;每个元素中的第二个元素分别表示自上一秒以来经过的时间(以微秒和纳秒为单位):

struct timespec {
        __kernel_time_t  tv_sec;                   /* seconds */
        long            tv_nsec;          /* nanoseconds */
};
#endif

struct timeval {
        __kernel_time_t          tv_sec;           /* seconds */
        __kernel_suseconds_t     tv_usec;  /* microseconds */
};

从时钟源读取的时间(计数器值)需要在某个地方累加和跟踪;在include/linux/timekeeper_internal.h,中定义的结构struct tk_read_base用于此目的:

struct tk_read_base {
        struct clocksource        *clock;
        cycle_t                  (*read)(struct clocksource *cs);
        cycle_t                  mask;
        cycle_t                  cycle_last;
        u32                      mult;
        u32                      shift;
        u64                      xtime_nsec;
        ktime_t                  base_mono;
};

include/linux/timekeeper_internal.h,中定义的结构struct timekeeper保持各种计时值。它是维护和操作不同时间表的计时数据的主要数据结构,例如单调和原始:

struct timekeeper {
        struct tk_read_base       tkr;
        u64                      xtime_sec;
        unsigned long           ktime_sec;
        struct timespec64 wall_to_monotonic;
        ktime_t                  offs_real;
        ktime_t                  offs_boot;
        ktime_t                  offs_tai;
        s32                      tai_offset;
        ktime_t                  base_raw;
        struct timespec64 raw_time;

        /* The following members are for timekeeping internal use */
        cycle_t                  cycle_interval;
        u64                      xtime_interval;
        s64                      xtime_remainder;
        u32                      raw_interval;
        u64                      ntp_tick;
        /* Difference between accumulated time and NTP time in ntp
        * shifted nano seconds. */
        s64                      ntp_error;
        u32                      ntp_error_shift;
        u32                      ntp_err_mult;
};

跟踪和维护时间

计时辅助程序timekeeping_get_ns()timekeeping_get_ns()有助于获得以纳秒为单位的世界时和地面时之间的校正系数(δt):

static inline u64 timekeeping_delta_to_ns(struct tk_read_base *tkr, u64 delta)
{
        u64 nsec;

        nsec = delta * tkr->mult + tkr->xtime_nsec;
        nsec >>= tkr->shift;

        /* If arch requires, add in get_arch_timeoffset() */
        return nsec + arch_gettimeoffset();
}

static inline u64 timekeeping_get_ns(struct tk_read_base *tkr)
{
        u64 delta;

        delta = timekeeping_get_delta(tkr);
        return timekeeping_delta_to_ns(tkr, delta);
}

例程logarithmic_accumulation()更新单声道、原始和 xtime 时间线;它将移位的周期间隔累积成移位的纳秒间隔。例程accumulate_nsecs_to_secs()struct tk_read_basextime_nsec字段中的纳秒累加到struct timekeeperxtime_sec中。这些程序有助于跟踪系统中的当前时间,并在kernel/time/timekeeping.c中定义:

static u64 logarithmic_accumulation(struct timekeeper *tk, u64 offset,
                                    u32 shift, unsigned int *clock_set)
{
        u64 interval = tk->cycle_interval << shift;
        u64 snsec_per_sec;

        /* If the offset is smaller than a shifted interval, do nothing */
        if (offset < interval)
                return offset;

        /* Accumulate one shifted interval */
        offset -= interval;
        tk->tkr_mono.cycle_last += interval;
        tk->tkr_raw.cycle_last  += interval;

        tk->tkr_mono.xtime_nsec += tk->xtime_interval << shift;
        *clock_set |= accumulate_nsecs_to_secs(tk);

        /* Accumulate raw time */
        tk->tkr_raw.xtime_nsec += (u64)tk->raw_time.tv_nsec << tk->tkr_raw.shift;
        tk->tkr_raw.xtime_nsec += tk->raw_interval << shift;
        snsec_per_sec = (u64)NSEC_PER_SEC << tk->tkr_raw.shift;
        while (tk->tkr_raw.xtime_nsec >= snsec_per_sec) {
                tk->tkr_raw.xtime_nsec -= snsec_per_sec;
                tk->raw_time.tv_sec++;
        }
        tk->raw_time.tv_nsec = tk->tkr_raw.xtime_nsec >> tk->tkr_raw.shift;
        tk->tkr_raw.xtime_nsec -= (u64)tk->raw_time.tv_nsec << tk->tkr_raw.shift;

        /* Accumulate error between NTP and clock interval */
        tk->ntp_error += tk->ntp_tick << shift;
        tk->ntp_error -= (tk->xtime_interval + tk->xtime_remainder) <<
                                                (tk->ntp_error_shift + shift);

        return offset;
}

kernel/time/timekeeping.c,中定义的另一个例程update_wall_time()负责维护墙时间。它使用当前时钟源作为参考来增加挂壁时间。

滴答和中断处理

为了提供编程接口,产生滴答的时钟设备通过结构struct clock_event_device进行抽象,在include/linux/clockchips.h中定义:

struct clock_event_device {
        void                    (*event_handler)(struct clock_event_device *);
        int                     (*set_next_event)(unsigned long evt, struct clock_event_device *);
        int                     (*set_next_ktime)(ktime_t expires, struct clock_event_device *);
        ktime_t                  next_event;
        u64                      max_delta_ns;
        u64                      min_delta_ns;
        u32                      mult;
        u32                      shift;
        enum clock_event_state    state_use_accessors;
        unsigned int            features;
        unsigned long           retries;

        int                     (*set_state_periodic)(struct  clock_event_device *);
        int                     (*set_state_oneshot)(struct clock_event_device *);
        int                     (*set_state_oneshot_stopped)(struct clock_event_device *);
        int                     (*set_state_shutdown)(struct clock_event_device *);
        int                     (*tick_resume)(struct clock_event_device *);

        void                    (*broadcast)(const struct cpumask *mask);
        void                    (*suspend)(struct clock_event_device *);
        void                    (*resume)(struct clock_event_device *);
        unsigned long           min_delta_ticks;
        unsigned long           max_delta_ticks;

        const char               *name;
        int                     rating;
        int                     irq;
        int                     bound_on;
        const struct cpumask       *cpumask;
        struct list_head  list;
        struct module             *owner;
} ____cacheline_aligned;

这里,event_handler是适当的例程,由框架指定,由低级处理程序调用来运行 tick。根据配置,该clock_event_device可能是基于periodicone-shot,ktime在这三种模式中,通过unsigned int features字段,使用以下任一宏设置滴答装置的适当操作模式:

#define CLOCK_EVT_FEAT_PERIODIC 0x000001
#define CLOCK_EVT_FEAT_ONESHOT 0x000002
#define CLOCK_EVT_FEAT_KTIME  0x000004

周期模式配置硬件每 1/HZ 秒产生一次滴答,而单次模式使硬件从当前时间经过特定数量的周期后产生滴答。

根据用例和操作模式,event_handler 可以是以下三种例程中的任何一种:

  • tick_handle_periodic() *、*是周期性滴答的默认处理程序,在kernel/time/tick-common.c 中定义。
  • tick_nohz_handler()是低分辨率中断处理程序,用于低分辨率模式。在kernel/time/tick-sched.c 中有定义。
  • hrtimer_interrupt()用于高分辨率模式,在kernel/time/hrtimer.c中定义。调用中断时,中断被禁用。

时钟事件设备通过在kernel/time/clockevents.c.中定义的例程clockevents_config_and_register()进行配置和注册

滴答装置

clock_event_device抽象为核心时序框架;我们需要对每个 CPU 的 tick 设备进行单独的抽象;这通过分别在kernel/time/tick-sched.hinclude/linux/percpu-defs.h中定义的结构struct tick_device和宏DEFINE_PER_CPU() *、*来实现:

enum tick_device_mode {
 TICKDEV_MODE_PERIODIC,
 TICKDEV_MODE_ONESHOT,
};

struct tick_device {
        struct clock_event_device *evtdev;
        enum tick_device_mode mode;
}

A tick_device可以是周期性的,也可以是一次性的。它是通过enum tick_device_mode 设定的。

软件定时器和延迟功能

软件定时器允许在持续时间到期时调用某个功能。有两种类型的计时器:内核使用的动态计时器和用户空间进程使用的间隔计时器。除了软件定时器之外,还有另一种常用的定时功能,称为延迟功能。延迟函数实现一个精确的循环,该循环根据(通常是)延迟函数的参数执行。

动态计时器

动态计时器可以随时创建和销毁,因此得名动态计时器。动态计时器由include/linux/timer.h中定义的struct timer_list对象表示:

struct timer_list {
        /*
        * Every field that changes during normal runtime grouped to the
        * same cacheline
        */
        struct hlist_node entry;
        unsigned long           expires;
        void                    (*function)(unsigned long);
        unsigned long           data;
        u32                      flags;

#ifdef CONFIG_LOCKDEP
        struct lockdep_map        lockdep_map;
#endif
};

系统中的所有定时器都由一个双向链表管理,并按照它们的到期时间排序,由 expires 字段表示。expires 字段指定定时器到期的持续时间。一旦当前jiffies值匹配或超过该字段的值,定时器就会衰减。通过输入字段,一个定时器被添加到这个定时器链表中。函数字段指向定时器到期时要调用的例程,数据字段保存要传递给函数的参数(如果需要)。过期字段不断与jiffies_64值进行比较,以确定计时器是否已过期。

动态计时器可以如下创建和激活:

  • 创建一个新的timer_list对象,比如说t_obj
  • 使用宏init_timer(&t_obj)初始化该定时器对象,在include/linux/timer.h.中定义
  • 用定时器到期时要调用的函数地址初始化函数字段。如果函数需要参数,也要初始化数据字段。
  • 如果计时器对象已经添加到计时器列表中,则通过调用在kernel/time/timer.c 中定义的函数mod_timer(&t_obj, <timeout-value-in-jiffies>)来更新过期字段。 ** 如果没有,初始化 expires 字段,并使用在/kernel/time/timer.c 中定义的add_timer(&t_obj)将定时器对象添加到定时器列表中。*

**内核会自动从定时器列表中删除一个失效的定时器,但是也有其他方法可以从列表中删除一个定时器。del_timer()del_timer_sync()例程以及kernel/time/timer.c中定义的宏del_singleshot_timer_sync()有助于做到这一点:

int del_timer(struct timer_list *timer)
{
        struct tvec_base *base;
        unsigned long flags;
        int ret = 0;

        debug_assert_init(timer);

        timer_stats_timer_clear_start_info(timer);
        if (timer_pending(timer)) {
                base = lock_timer_base(timer, &flags);
                if (timer_pending(timer)) {
                        detach_timer(timer, 1);
                        if (timer->expires == base->next_timer &&
                            !tbase_get_deferrable(timer->base))
                                base->next_timer = base->timer_jiffies;
                        ret = 1;
                }
                spin_unlock_irqrestore(&base->lock, flags);
        }

        return ret;
}

int del_timer_sync(struct timer_list *timer)
{
#ifdef CONFIG_LOCKDEP
        unsigned long flags;

        /*
        * If lockdep gives a backtrace here, please reference
        * the synchronization rules above.
        */
        local_irq_save(flags);
        lock_map_acquire(&timer->lockdep_map);
        lock_map_release(&timer->lockdep_map);
        local_irq_restore(flags);
#endif
        /*
        * don't use it in hardirq context, because it
        * could lead to deadlock.
        */
        WARN_ON(in_irq());
        for (;;) {
                int ret = try_to_del_timer_sync(timer);
                if (ret >= 0)
                        return ret;
                cpu_relax();
        }
}

#define del_singleshot_timer_sync(t) del_timer_sync(t)

del_timer()删除活动和非活动计时器。在 SMP 系统中特别有用,del_timer_sync()停用定时器并等待,直到处理程序在其他 CPU 上完成执行。

带有动态计时器的比赛条件

删除计时器时,必须特别小心,因为计时器函数可能正在操作一些动态不可分配的资源。如果在停用计时器之前释放资源,则当它所操作的资源根本不存在时,计时器功能有可能被调用,从而导致数据损坏。因此,为了避免这种情况,必须在释放任何资源之前停止计时器。下面的代码片段复制了这种情况;RESOURCE_DEALLOCATE()这里可以是任何相关的资源解除分配例程:

...
del_timer(&t_obj);
RESOURCE_DEALLOCATE();
....

然而,这种方法仅适用于单处理器系统。在 SMP 系统中,很有可能当计时器停止时,它的功能可能已经在另一个 CPU 上运行。在这样的场景下,del_timer()一返回,资源就会被释放,而定时器功能还在其他 CPU 上操纵;一点也不理想。del_timer_sync()修复了这个问题:停止定时器后,等待直到定时器功能在另一个 CPU 上完成执行。del_timer_sync()在定时器功能可以自动重启的情况下很有用。如果定时器功能没有重新激活定时器,应该使用更简单更快速的宏del_singleshot_timer_sync()

动态定时器处理

软件计时器复杂且耗时,因此不应由计时器 ISR 处理。相反,它们应该由一个称为TIMER_SOFTIRQ的可推迟的下半部分软 irq 例程来执行,其例程在kernel/time/timer.c中定义:

static __latent_entropy void run_timer_softirq(struct softirq_action *h)
{
        struct timer_base *base = this_cpu_ptr(&timer_bases[BASE_STD]);

        base->must_forward_clk = false;

        __run_timers(base);
        if (IS_ENABLED(CONFIG_NO_HZ_COMMON) && base->nohz_active)
                __run_timers(this_cpu_ptr(&timer_bases[BASE_DEF]));
}

延迟函数

当超时时间相对较长时,计时器很有用;在需要较短持续时间的所有其他使用情况下,使用延迟函数来代替。在处理存储设备(即闪存EEPROM )等硬件时,设备驱动程序必须等到设备完成写入和擦除等硬件操作,这一点至关重要,在大多数情况下,这一时间范围在几微秒到几毫秒之间。继续执行其他指令而不等待硬件完成这些操作将导致不可预测的读/写操作和数据损坏。在这种情况下,延迟函数就派上了用场。内核通过ndelay()udelay()mdelay()例程和宏提供如此短的延迟,它们分别接收纳秒、微秒和毫秒的参数。

*在include/linux/delay.h中可以找到以下功能:

static inline void ndelay(unsigned long x)
{
        udelay(DIV_ROUND_UP(x, 1000));
}

这些功能可以在arch/ia64/kernel/time.c中找到:

static void
ia64_itc_udelay (unsigned long usecs)
{
        unsigned long start = ia64_get_itc();
        unsigned long end = start + usecs*local_cpu_data->cyc_per_usec;

        while (time_before(ia64_get_itc(), end))
                cpu_relax();
}

void (*ia64_udelay)(unsigned long usecs) = &ia64_itc_udelay;

void
udelay (unsigned long usecs)
{
        (*ia64_udelay)(usecs);
}

POSIX 时钟

POSIX 为多线程和实时用户空间应用提供软件定时器,称为 POSIX 定时器。POSIX 提供以下时钟:

  • CLOCK_REALTIME:这个时钟代表系统中的实时。也称为挂钟时间,它类似于挂钟上的时间,用于时间戳以及向用户提供实际时间。这个钟是可以修改的。

  • CLOCK_MONOTONIC:该时钟记录系统启动后经过的时间。它不断增加,并且不可由任何进程或用户修改。由于其单调性,它是确定两个时间事件之间时间差的首选时钟。

  • CLOCK_BOOTTIME:该时钟与 CLOCK_MONOTONIC 相同;但是,它包括暂停所花费的时间。

这些时钟可以通过以下 POSIX 时钟例程来访问和修改(如果所选时钟允许的话),这些例程在time.h标题中定义:

  • int clock_getres(clockid_t clk_id, struct timespec *res);
  • int clock_gettime(clockid_t clk_id, struct timespec *tp);
  • int clock_settime(clockid_t clk_id, const struct timespec *tp);

功能clock_getres()获取 clk_id 指定的时钟分辨率(精度)。如果分辨率为非空,则存储在分辨率指向的struct timespec中。功能clock_gettime()clock_settime()读取并设置 clk_id 指定的时钟时间。 clk_id 可以是任何一个 POSIX 时钟:CLOCK_REALTIMECLOCK_MONOTONIC等等。

CLOCK_REALTIME_COARSE

CLOCK_MONOTONIC_COARSE

这些 POSIX 例程都有相应的系统调用,即sys_clock_getres(), sys_ clock_gettime()sys_clock_settime *。*所以每次调用这些例程时,都会发生从用户模式到内核模式的上下文切换。如果频繁调用这些例程,上下文切换会导致系统性能低下。为了避免上下文切换,POSIX 时钟的两个粗略变体被实现为 vDSO(虚拟动态共享对象)库:

vDSO 是一个带有选定内核空间例程的小型共享库,内核将这些例程映射到用户空间应用的地址空间中,以便这些内核空间例程可以由它们在进程中从用户空间直接调用。C 库调用 vDSO,因此用户空间应用可以通过标准函数以通常的方式进行编程,C 库将利用 vDSO 提供的功能,而无需使用任何系统调用接口,从而避免任何用户模式-内核模式上下文切换和系统调用开销。作为一个 vDSO 实现,这些粗糙的变体速度更快,分辨率为 1 毫秒。

摘要

在这一章中,除了理解 Linux 时间的基本方面、它的基础设施和它的度量之外,我们还详细研究了内核提供的驱动基于时间的事件的大多数例程。我们还简要地看了 POSIX 时钟及其一些关键的时间访问和修改例程。然而,有效的时间驱动程序依赖于这些例程的仔细和计算的使用。

在下一章中,我们将简要介绍动态内核模块的管理。************