Timers and time management in the Linux kernel. Part 7.

这篇文章 Timers and time management in the Linux kernel. Part 7. 是出自 linux-insides一书中 Timers and time management 章节

内核版本比对5.7 进行了相关调整, 增加相关备注

Linux内核中与时间相关的系统调用

这是第七章也是最后一章chapter, 它描述了Linux内核中计数器和时间管理相关的内容. 在前面的章节 part, 我们在 x86_64: High Precision Event TimerTime Stamp Counter. 内部时间管理是Linux内核中一个有趣的部分, 但是,当然,不仅内核需要time概念. 我们的程序还需要知道时间。这一部分中,我们将考虑实际一些与时间管理相关的 system calls. 这些系统调用:

  • clock_gettime;
  • gettimeofday;
  • nanosleep.

我们将从一个简单的用户空间C 程序开始, 并从standard library 函数执行某些系统调用。由于每个 architecture 提供了自己的某些系统调用实现,因此我们将仅考虑x86_64系统调用的特定实现,因为本书与此体系结构相关。

Additionally, we will not consider the concept of system calls in this part, but only implementations of these three system calls in the Linux kernel. If you are interested in what is a system call, there is a special chapter about this.

此外, 在本部分中, 我们将不考虑系统调用的的概念, 而仅考虑这三个系统调用Linux内核中的实现, 如果你对system call感兴趣, 可以参考这个章节 chapter about this.

因此,让我们从gettimeofday系统调用开始。

gettimeofday 系统调用的实现

从名称gettimeofday可以理解,该函数返回当前时间。 首先,让我们看下面的简单示例:

#include <time.h>
#include <sys/time.h>
#include <stdio.h>

int main(int argc, char **argv)
{
    char buffer[40];
    struct timeval time;

    gettimeofday(&time, NULL);

    strftime(buffer, 40, "Current date/time: %m-%d-%Y/%T", localtime(&time.tv_sec));
    printf("%s\n",buffer);

    return 0;
}

如您所见,这里我们调用gettimeofday函数,该函数带有两个参数。 第一个参数是指向timeval结构的指针,它表示经过的时间:

struct timeval {
    time_t      tv_sec;     /* seconds */
    suseconds_t tv_usec;    /* microseconds */
};

函数gettimeofday的第二个参数是指向表示时区的timezone结构的指针。 在我们的示例中,我们将timeval time的地址传递给gettimeofday函数,Linux内核填充给定的timeval结构并将其返回给我们。 另外,我们使用strftime函数设置时间格式,以获得比已经经过的微秒可具有可读性。 让我们看一下结果:

~$ gcc date.c -o date
~$ ./date
Current date/time: 03-26-2016/16:42:02

你可能已经知道, 用户空间应用程序不会直接从内核空间调用系统调用。 在调用实际的系统调用条目之前,我们从标准库中调用一个函数 , 这个例子他使用的是glibc, gettimeofday 函数位于 sysdeps/unix/sysv/linux/x86/gettimeofday.c 文件中。
. gettimeofday 不是通常的系统调用。 它位于成为VDSO的特殊区域中 (你可以在这里part, 了解更多的相关信息)。

gettimeofdayglibc实现试图解析给定的符号;在我们的例子中,通过内部函数 _dl_vdso_vsym 调用, 该符号为__vdso_gettimeofday。如果符号无法解析, 返回NULL, 我们将回退到常规的系统调用。i

return (_dl_vdso_vsym ("__vdso_gettimeofday", &linux26)
  ?: (void*) (&__gettimeofday_syscall));

gettimeofday入口位于 arch/x86/entry/vdso/vclock_gettime.c 源文件中, 我们能看到函数gettimeofday 的弱应用定义 __vdso_gettimeofday:

int gettimeofday(struct timeval *, struct timezone *)
    __attribute__((weak, alias("__vdso_gettimeofday")));

__vdso_gettimeofday中定义/lib/vdso/gettimeofday.c中,如果给定的timeval不为null,则调用do_hres函数:

static __maybe_unused int
__cvdso_gettimeofday_data(const struct vdso_data *vd,
              struct __kernel_old_timeval *tv, struct timezone *tz)
{

    if (likely(tv != NULL)) {
        struct __kernel_timespec ts;

        if (do_hres(&vd[CS_HRES_COARSE], CLOCK_REALTIME, &ts))
            return gettimeofday_fallback(tv, tz);

        tv->tv_sec = ts.tv_sec;
        tv->tv_usec = (u32)ts.tv_nsec / NSEC_PER_USEC;
    }

    if (unlikely(tz != NULL)) {
        if (IS_ENABLED(CONFIG_TIME_NS) &&
            vd->clock_mode == VDSO_CLOCKMODE_TIMENS)
            vd = __arch_get_timens_vdso_data();

        tz->tz_minuteswest = vd[CS_HRES_COARSE].tz_minuteswest;
        tz->tz_dsttime = vd[CS_HRES_COARSE].tz_dsttime;
    }

    return 0;
}

如果 do_hres 调用失败, 通过调用_gettimeofday_fallback,实现系统调用

static __always_inline
long gettimeofday_fallback(struct __kernel_old_timeval *_tv,
               struct timezone *_tz)
{
    long ret;

    asm("syscall" : "=a" (ret) :
        "0" (__NR_gettimeofday), "D" (_tv), "S" (_tz) : "memory");

    return ret;
}

do_hres 函数从vdso_data中获取时间数据, vdso_data 定义在/include/vdso/datapage.h 头文件中, 包含了vdso_timestamp 结构的映射以及与系统当前时钟源相关的两个字段。 该函数用来自vdso_data中的vdso_timestamp的值填充给定的timeval结构,该值包含与时间相关的数据,该数据通过计时器中断进行更新。

另外在5.7版本中可以看到 CONFIG_TIME_NS: TIME namespace, 支持从中获取时间信息
这里使用的clockid 为CLOCK_REALTIME

...
while (unlikely((seq = READ_ONCE(vd->seq)) & 1)) {
    if (IS_ENABLED(CONFIG_TIME_NS) &&
        vd->clock_mode == VDSO_CLOCKMODE_TIMENS)
        return do_hres_timens(vd, clk, ts);
    cpu_relax();
}
...

static int do_hres_timens(const struct vdso_data *vdns, clockid_t clk,
              struct __kernel_timespec *ts)
{
    const struct vdso_data *vd = __arch_get_timens_vdso_data();
    const struct timens_offset *offs = &vdns->offset[clk];
    const struct vdso_timestamp *vdso_ts;
    u64 cycles, last, ns;
    u32 seq;
    s64 sec;

    if (clk != CLOCK_MONOTONIC_RAW)
        vd = &vd[CS_HRES_COARSE];
    else
        vd = &vd[CS_RAW];
    vdso_ts = &vd->basetime[clk];

    do {
        seq = vdso_read_begin(vd);

        if (unlikely(!vdso_clocksource_ok(vd)))
            return -1;

        cycles = __arch_get_hw_counter(vd->clock_mode);
        ns = vdso_ts->nsec;
        last = vd->cycle_last;
        ns += vdso_calc_delta(cycles, last, vd->mask, vd->mult);
        ns = vdso_shift_ns(ns, vd->shift);
        sec = vdso_ts->sec;
    } while (unlikely(vdso_read_retry(vd, seq)));

    /* Add the namespace offset */
    sec += offs->sec;
    ns += offs->nsec;

    /*
     * Do this outside the loop: a race inside the loop could result
     * in __iter_div_u64_rem() being extremely slow.
     */
    ts->tv_sec = sec + __iter_div_u64_rem(ns, NSEC_PER_SEC, &ns);
    ts->tv_nsec = ns;

    return 0;
}

首先,获取vdso_data中的vdso_timestamp信息, 通过一系列运算,计算出最终结果。

...
const struct vdso_timestamp *vdso_ts = &vd->basetime[clk];
...
do {
    while (unlikely((seq = READ_ONCE(vd->seq)) & 1)) {
        if (IS_ENABLED(CONFIG_TIME_NS) &&
            vd->clock_mode == VDSO_CLOCKMODE_TIMENS)
            return do_hres_timens(vd, clk, ts);
        cpu_relax();
    }
    smp_rmb();

    if (unlikely(!vdso_clocksource_ok(vd)))
        return -1;

    cycles = __arch_get_hw_counter(vd->clock_mode);
    ns = vdso_ts->nsec;
    last = vd->cycle_last;
    ns += vdso_calc_delta(cycles, last, vd->mask, vd->mult);
    ns = vdso_shift_ns(ns, vd->shift);
    sec = vdso_ts->sec;
} while (unlikely(vdso_read_retry(vd, seq)));

ts->tv_sec = sec + __iter_div_u64_rem(ns, NSEC_PER_SEC, &ns);
ts->tv_nsec = ns;

当我们访问时,我们用vdso_timestamp.sec赋值ts->tv_sec, 以秒为单位存储当前时间 , 在Linux内核中的计时子系统初始化期间从实时时钟real time clock 获得,值相同,但以纳秒为单位。在这段代码的最后,我们只使用结果值填充给定__kernel_timespec 的结构。

这是关于 gettimeofday 下面介绍clock_gettime.

clock_gettime 系统调用的实现

clock_gettime通过第二个参数获取指定的时间。 通常 clock_gettime有两个参数:

  • clk_id - 时钟标识符;
  • timespec - timespec 结构指针,也就是获取当前时间.

下面看一个简单的例子:

#include <time.h>
#include <sys/time.h>
#include <stdio.h>

int main(int argc, char **argv)
{
    struct timespec elapsed_from_boot;

    clock_gettime(CLOCK_BOOTTIME, &elapsed_from_boot);

    printf("%d - seconds elapsed from boot\n", elapsed_from_boot.tv_sec);

    return 0;
}

打印 uptime 信息, 从启动到现在经过的秒数:

~$ gcc uptime.c -o uptime
~$ ./uptime
14180 - seconds elapsed from boot

我们也可以通过 uptime 获取相关结果:

~$ uptime
up  3:56

elapsed_from_boot.tv_sec 表示从起点到现在过去的时间, 因此:

>>> 14180 / 60
236
>>> 14180 / 60 / 60
3
>>> 14180 / 60 % 60
56

clock_id 可以设置以下值:

  • CLOCK_REALTIME - system wide clock which measures real or wall-clock time;
  • CLOCK_REALTIME_COARSE - faster version of the CLOCK_REALTIME;
  • CLOCK_MONOTONIC - represents monotonic time since some unspecified starting point;
  • CLOCK_MONOTONIC_COARSE - faster version of the CLOCK_MONOTONIC;
  • CLOCK_MONOTONIC_RAW - the same as the CLOCK_MONOTONIC but provides non NTP adjusted time.
  • CLOCK_BOOTTIME - the same as the CLOCK_MONOTONIC but plus time that the system was suspended;
  • CLOCK_PROCESS_CPUTIME_ID - per-process time consumed by all threads in the process;
  • CLOCK_THREAD_CPUTIME_ID - thread-specific clock.

clock_gettime也不是通常的系统调用,但是作为gettimeofday,此系统调用位于vDSO区域中。 该系统调用的条目位于与gettimeofday相同的源代码文件中- /arch/x86/entry/vdso/vclock_gettime.c)`.

The Implementation of the clock_gettime depends on the clock id. If we have passed the CLOCK_REALTIME clock id, the do_realtime function will be called:

clock_gettime的实现取决于时钟id。vdso_clock_gettime调用cvdso_clock_gettime , 该函数位于/lib/vdso/gettimeofday.c, 之后调用__cvdso_clock_gettime_common, 如果处理失败通过clock_gettime_fallback系统调用方式完成

static __always_inline
long clock_gettime_fallback(clockid_t _clkid, struct __kernel_timespec *_ts)
{
    long ret;

    asm ("syscall" : "=a" (ret), "=m" (*_ts) :
         "0" (__NR_clock_gettime), "D" (_clkid), "S" (_ts) :
         "rcx", "r11");

    return ret;
}

位于/arch/x86/include/asm/vdso/gettimeofday.h

函数根据clockid的被范围三类 VDSO_HRES, VDSO_COARSE, VDSO_RAW, 针对VDSO_COARSE调用do_corase 函数, 其他调用do_hres

#define VDSO_BASES (CLOCK_TAI + 1)
#define VDSO_HRES  (BIT(CLOCK_REALTIME)        | \
             BIT(CLOCK_MONOTONIC)       | \
             BIT(CLOCK_BOOTTIME)        | \
             BIT(CLOCK_TAI))
#define VDSO_COARSE    (BIT(CLOCK_REALTIME_COARSE) | \
             BIT(CLOCK_MONOTONIC_COARSE))
#define VDSO_RAW   (BIT(CLOCK_MONOTONIC_RAW))

#define CS_HRES_COARSE 0
#define CS_RAW     1

位于 /include/vdso/datapage.h

static __maybe_unused int
__cvdso_clock_gettime_common(const struct vdso_data *vd, clockid_t clock,
                 struct __kernel_timespec *ts)
{
    u32 msk;

    /* Check for negative values or invalid clocks */
    if (unlikely((u32) clock >= MAX_CLOCKS))
        return -1;

    /*
     * Convert the clockid to a bitmask and use it to check which
     * clocks are handled in the VDSO directly.
     */
    msk = 1U << clock;
    if (likely(msk & VDSO_HRES))
        vd = &vd[CS_HRES_COARSE];
    else if (msk & VDSO_COARSE)
        return do_coarse(&vd[CS_HRES_COARSE], clock, ts);
    else if (msk & VDSO_RAW)
        vd = &vd[CS_RAW];
    else
        return -1;

    return do_hres(vd, clock, ts);
}

可以看到 do_coarse处理流程和dohres,流程类似,都是先判度是否使用名字空间存储时间信息, 满足则调用do{}_timens

static __always_inline int do_coarse(const struct vdso_data *vd, clockid_t clk,
                     struct __kernel_timespec *ts)
{
    const struct vdso_timestamp *vdso_ts = &vd->basetime[clk];
    u32 seq;

    do {
        while ((seq = READ_ONCE(vd->seq)) & 1) {
            if (IS_ENABLED(CONFIG_TIME_NS) &&
                vd->clock_mode == VDSO_CLOCKMODE_TIMENS)
                return do_coarse_timens(vd, clk, ts);
            cpu_relax();
        }
        smp_rmb();

        ts->tv_sec = vdso_ts->sec;
        ts->tv_nsec = vdso_ts->nsec;
    } while (unlikely(vdso_read_retry(vd, seq)));

    return 0;
}

do_coarse_timensdo_hres_timens 非常类似

static int do_coarse_timens(const struct vdso_data *vdns, clockid_t clk,
                struct __kernel_timespec *ts)
{
    const struct vdso_data *vd = __arch_get_timens_vdso_data();
    const struct vdso_timestamp *vdso_ts = &vd->basetime[clk];
    const struct timens_offset *offs = &vdns->offset[clk];
    u64 nsec;
    s64 sec;
    s32 seq;

    do {
        seq = vdso_read_begin(vd);
        sec = vdso_ts->sec;
        nsec = vdso_ts->nsec;
    } while (unlikely(vdso_read_retry(vd, seq)));

    sec += offs->sec;
    nsec += offs->nsec;

    ts->tv_sec = sec + __iter_div_u64_rem(nsec, NSEC_PER_SEC, &nsec);
    ts->tv_nsec = nsec;
    return 0;
}

在上一节描述了gettimeofday函数实现的一些信息。 这里只有一个区别,我们的gettimeofday默认使用clockid CLOCK_REALTIME, clock_gettimeclockid则是可以指定的.如果vdso调用成功, 则这两个函数结果是一致的。

vdso的值是在 tick , 通过timekeeping_xxx更新vdso中的相关时间信息

nanosleep 系统调用的实现

我们列表中的最后一个系统调用是nanosleep。 从名称可以理解,此功能提供了sleeping功能。 让我们看下面的简单示例:

#include <time.h>
#include <stdlib.h>
#include <stdio.h>

int main (void)
{    
   struct timespec ts = {5,0};

   printf("sleep five seconds\n");
   nanosleep(&ts, NULL);
   printf("end of sleep\n");

   return 0;
}

编译执行得到如下结果:

~$ gcc sleep_test.c -o sleep
~$ ./sleep
sleep five seconds
end of sleep

第二行5秒后显示。

nanosleepgettimeofdayclock_gettime函数不同,没有位于VDSO区域,因此,下面分析下标准库是如何调用位于内核空间的系统调用的。。nanosleep系统调用的实现将通过 syscall 指令实现。在执行 syscall 指令之前将参数按照 System V Application Binary Interface接口描述:, 传入处理器的registers (寄存器)中。

  • rdi - first parameter;
  • rsi - second parameter;
  • rdx - third parameter;
  • r10 - fourth parameter;
  • r8 - fifth parameter;
  • r9 - sixth parameter.

nanosleep 有两个参数, 两个timespec 结构指针.系统调用暂停调用线程, 直到给定的超时时间过去未为止. 此外,如果信号中断其执行, 两个参数,第一个代表其睡眠时间,第二个参数代表其剩余未睡眠时间。

#include <time.h>
#include <stdlib.h>
#include <stdio.h>
#include <signal.h>

void donothing()
{
  return;
}

int main (void)
{
  struct timespec ts = { 10, 0 };
  struct timespec ts2 = { 5, 0 };

  signal(SIGINT, donothing);
  printf ("sleep five seconds\n");
  nanosleep (&ts, &ts2);

  printf("ts { %d %d }\n", ts.tv_sec, ts.tv_nsec);
  printf("ts2{ %d %d }\n", ts2.tv_sec, ts2.tv_nsec);
  printf ("end of sleep\n");

  return 0;
}

编译一下, 按一下 CTRL+C, 可以看到数据结果;

~$ gcc sleep_test2.c -o sleep2
~$ ./sleep2
sleep five seconds
^Cts { 10 0 }
ts2{ 9 471858528 }
end of sleep

nanosleep 有两个参数:

int nanosleep(const struct timespec *req, struct timespec *rem);

要调用系统调用,我们需要将req放入rdi寄存器,并将rem参数放入rsi寄存器。 glibc在sysdeps/unix/sysv/linux/x86_64/sysdep.h头文件中的INTERNAL_SYSCALL宏中完成这些工作。

# define INTERNAL_SYSCALL(name, err, nr, args...) \
  INTERNAL_SYSCALL_NCS (__NR_##name, err, nr, ##args)

其中包含系统调用的名称_NR##name,在执行系统调用期间可能出现的错误err,系统调用的编号nr(可以在system calls table中找到的所有X86_64的系统调用)和某些系统调用的参数##argsINTERNAL_SYSCALL宏只是扩展到INTERNAL_SYSCALL_NCS宏的调用,该宏准备系统调用的参数(以正确的顺序将其放入处理器寄存器),执行syscall指令并返回结果:

# define INTERNAL_SYSCALL_NCS(name, err, nr, args...)      \
  ({                                                                          \
    unsigned long int resultvar;                                              \
    LOAD_ARGS_##nr (args)                                                     \
    LOAD_REGS_##nr                                                            \
    asm volatile (                                                            \
    "syscall\n\t"                                                             \
    : "=a" (resultvar)                                                        \
    : "0" (name) ASM_ARGS_##nr : "memory", REGISTERS_CLOBBERED_BY_SYSCALL);   \
    (long int) resultvar; })

LOADARGS##nr 调用 宏LOAD_ARGS_N , 其中 N 是系统调用的参数.在这个例子里是 LOAD_ARGS_2. 这些宏扩展未一下内容:
rdi设置为第一个参数, rsi设置为第二个参数.

# define LOAD_REGS_TYPES_1(t1, a1)                     \
  register t1 _a1 asm ("rdi") = __arg1;                    \
  LOAD_REGS_0

# define LOAD_REGS_TYPES_2(t1, a1, t2, a2)                 \
  register t2 _a2 asm ("rsi") = __arg2;                    \
  LOAD_REGS_TYPES_1(t1, a1)
...
...
...

在执行syscall指令后,将进行上下文切换context switch ,并且内核会将执行转移到系统调用处理程序。 nanosleep系统调用的系统调用处理程序位于 kernel/time/hrtimer.c 源代码文件中,并使用SYSCALL_DEFINE2宏进行了函数定义:

#ifdef CONFIG_64BIT

SYSCALL_DEFINE2(nanosleep, struct __kernel_timespec __user *, rqtp,
        struct __kernel_timespec __user *, rmtp)
{
    struct timespec64 tu;

    if (get_timespec64(&tu, rqtp))
        return -EFAULT;

    if (!timespec64_valid(&tu))
        return -EINVAL;

    current->restart_block.nanosleep.type = rmtp ? TT_NATIVE : TT_NONE;
    current->restart_block.nanosleep.rmtp = rmtp;
    return hrtimer_nanosleep(timespec64_to_ktime(tu), HRTIMER_MODE_REL,
                 CLOCK_MONOTONIC);
}

#endif

您可以在有关系统调用的章节 chapter 中阅读有关SYSCALL_DEFINE2宏的更多信息。 如果我们看一下nanosleep系统调用的实现,首先我们将看到它是从get_timespec64函数的调用开始的。 此函数将给定的数据从用户空间复制到内核空间, 并判断是否是32bit进程的系统该调用()。 在我们的例子中,我们将超时值复制到睡眠超时值到内核空间timespec64结构,并通过调用timespec64_valid函数来检查给定的值是否有效:

#define NSEC_PER_SEC   1000000000L

static inline bool timespec64_valid(const struct timespec64 *ts)
{
    if (ts->tv_sec < 0)
        return false;
    if ((unsigned long)ts->tv_nsec >= NSEC_PER_SEC)
        return false;
    return true;
}

它只是检查 timespec64 不代表1970年之前的日期, 纳秒不会溢出1 秒。 nanosleep 函数最终会调用同文件中的hrtimer_nanosleep, hrtimer_nanosleep 函数创建一个计时器 timer 并调用 do_nanosleep 函数. do_nanosleep函数做主要的工作, 这个函数提供循环处理:

do {
    set_current_state(TASK_INTERRUPTIBLE);
    hrtimer_start_expires(&t->timer, mode);

    if (likely(t->task))
        freezable_schedule();

} while (t->task && !signal_pending(current));

__set_current_state(TASK_RUNNING);
return t->task == NULL;
do {
    set_current_state(TASK_INTERRUPTIBLE);
    hrtimer_sleeper_start_expires(t, mode);

    if (likely(t->task))
        freezable_schedule();

    hrtimer_cancel(&t->timer);
    mode = HRTIMER_MODE_ABS;

} while (t->task && !signal_pending(current));

在睡眠期间冻结当前任务。 在为当前任务设置TASK_INTERRUPTIBLE标志之后,hrtimer_sleeper_start_expires函数将在当前处理器上启动高精度的时器。 如果给定的高精度计时器将过期,因此该任务将再次运行。

小结

这是本章chapter 第七部分的结尾,它描述了Linux内核中与计时器和计时器管理相关的内容。 在上一部分中,我们看到了x86_64特定的时钟源。 如我一开始所写,这部分是本章的最后部分。 在本章中,我们看到了与时间管理相关的重要概念,例如clocksourceclockevents框架,jiffies计数器等。 当然,这并不涵盖Linux内核中的所有时间管理。 其中的许多部分主要与调度相关,我们将在另一章中看到。

如果您有任何问题或建议,请随时在twitter 0xAX上ping我,给我发电子邮件email或者只创建问题

请注意,英语不是我的第一语言,我很抱歉给您带来不便。如果您发现任何错误,请将PR发送至linux-insides

相关链接

Be First to Comment

发表评论

电子邮件地址不会被公开。 必填项已用*标注