这篇文章 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 Timer 和Time 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, 了解更多的相关信息)。
gettimeofday
的glibc
实现试图解析给定的符号;在我们的例子中,通过内部函数 _dl_vdso_vsym
调用, 该符号为__vdso_gettimeofday
。如果符号无法解析, 返回NULL
, 我们将回退到常规的系统调用。
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 theCLOCK_REALTIME
;CLOCK_MONOTONIC
– represents monotonic time since some unspecified starting point;CLOCK_MONOTONIC_COARSE
– faster version of theCLOCK_MONOTONIC
;CLOCK_MONOTONIC_RAW
– the same as theCLOCK_MONOTONIC
but provides non NTP adjusted time.CLOCK_BOOTTIME
– the same as theCLOCK_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 theCLOCK_REALTIME
clock id, thedo_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
处理流程和do_hres
,流程类似,都是先判度是否使用名字空间存储时间信息, 满足则调用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_timens
与do_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_gettime
的clockid
则是可以指定的.如果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
秒后显示。
nanosleep
与gettimeofday
和clock_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
的系统调用)和某些系统调用的参数##args
。 INTERNAL_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; })
宏 LOAD_ARGS_##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)
...
...
...
在执行syscal
l指令后,将进行上下文切换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特定的时钟源。 如我一开始所写,这部分是本章的最后部分。 在本章中,我们看到了与时间管理相关的重要概念,例如clocksource
和clockevents
框架,jiffies
计数器等。 当然,这并不涵盖Linux内核中的所有时间管理。 其中的许多部分主要与调度相关,我们将在另一章中看到。
如果您有任何问题或建议,请随时在twitter 0xAX上ping我,给我发电子邮件email或者只创建问题。
请注意,英语不是我的第一语言,我很抱歉给您带来不便。如果您发现任何错误,请将PR发送至linux-insides。
Be First to Comment