问题:

在工作中遇到一个需要调整容器内应用时间的需求,要求不同的容器可以使用不同的时间,但是众所周知,在容器内部,正常权限是无法采用date命令去修改时间的,如果执行date -s去修改的话,会出现如下报错:

怎么看容器ip_Linux怎么查询全部容器时间

同时hwclock也无法使用:

怎么看容器ip_Linux怎么查询全部容器时间_02

当然可以通过给容器添加–privileged来实现需求:

怎么看容器ip_linux_03

但是官方不建议采用的方式肯定是有原因,因为这样会直接影响到容器所在主机的时间,如果单台主机还好,但是试想下如果是kubernetes集群呢,会考虑因为单个容器或者几个容器去影响到集群内部主机的时间吗?

答案肯定是否定的,接下来我们首先会从源码级别分析下linux的时间子系统即系统时间是如何产生的,再分析下为什么修改单个容器的时间会影响到整个主机的时间,最后会针对这种跟时间相关的需求给出几种解决方案。

一、linux系统中的时间机制

在linux系统中有两种时钟,一种是硬件时钟,一种是系统时钟:

1.1 The Hardware Clock

硬件时钟跟运行在cpu上的程序是独立不相关的,甚至在服务器关机之后仍然可以正常运行,这就保证了服务器时间的正常运行,硬件时间也有着各种各样的称呼,例如:hardware clock, real time clock, RTC, BIOS clock以及CMOS clock等,在目前主流的服务器都采用RTC芯片实现:

怎么看容器ip_系统时间_04

该芯片采用32768HZ的晶振来满足计时的需求,同时拥有独立的电源可以保证断电之后依然可以正常计时,在系统中可以看到硬件时钟的值:

怎么看容器ip_初始化_05

1.2 The System Clock

在linux 内核中还有一个称为系统时钟或者系统时间的概念,这就是我们平时在系统中经常接触到时间,也是应用程序在执行与时间相关的操作会用到的时间,它只是在系统运行时存在,其记录形式为UTC时间(the number of seconds since 00:00:00 January 1, 1970 UTC)。

硬件时钟和系统时间的关系应该如何定义呢?

硬件时钟是用来保证在操作系统关机之后仍然可以正常计时必要硬件,而系统时间是我们在日常操作中才会经常使用到的时间,仅仅在操作系统初始化时,操作系统才会去RTC芯片中拿到硬件时钟的值,之后便是独立运行和独立计时。

二、内核中的时间

由上面的分析,我们可以知道在正常应用中是不会去使用硬件时间,一般都会考虑用系统时间,所以接下来看下应用是如何使用系统时间的。

在linux中,应用采用syscall的方式去访问内核中的数据,我们考虑从syscall入手来看时间的获取流程。

怎么看容器ip_怎么看容器ip_06

首先说明,本次分析的内核源码采用3.10版本的源码。

获取时间使用的syscall-gettimeofday:

怎么看容器ip_linux_07

可以看到之后调用的方法是内核中常用命名规则的方法:do_gettimeofday,这里有个小技巧,copy_to_user方法,这个我们随后分析这个方法的巧妙之处,我们先看下do_gettimeofday的方法实现:

怎么看容器ip_linux_08

可以看到该方法中通过getnstimeofday获取了系统的时间,再看下getnstimeofday的定义:

怎么看容器ip_Linux怎么查询全部容器时间_09

怎么看容器ip_linux_10

这个方法很简单,就是调用了__getnstimeofday方法,接着往下看:

怎么看容器ip_系统时间_11

在__getnstimeofday方法中,可以看到方法拿到timekeeper结构体中的xtime_sec以及xtime_nsecs的值。

这个timekeeper到底是什么东西呢?

在之前旧版本的内核定义了很多零散的全局变量来管理linux kernel中的各种系统clock,后来,内核统一定义了struct timekeeper数据结构来管理各种系统时钟,定义如下:

怎么看容器ip_初始化_12

可以看到,在__getnstimeofday里面拿到的两个值分别为Current CLOCK_REALTIME time in seconds(xtime_sec) 和 Clock shifted nano seconds(xtime_nsec)。

接下来,我们就得看下结构体timekeeper里面的数据是哪里来的?

timekeeping初始化代码在timekeeping_init中,在系统初始化的时候(start_kernel)会调用该函数进行timekeeping的初始化。

怎么看容器ip_系统时间_13

可以看到在初始化过程中,read_persistent_clock方法会去persistent clock里面获取当前的时间值来进行初始化timekeeper,这个persistent clock一般来说指的就是RTC芯片,这也是我们上文谈到的硬件时钟与系统时钟的关系。

怎么看容器ip_初始化_14

在x86体系中,该方法位于内核源码的linux/arch/x86/kernel/rtc.c内,再看最后一个函数:get_wallclock:

怎么看容器ip_怎么看容器ip_15

这段代码位于 linux/arch/x86/kernel/x86_init.c内,直接说明了系统时间的最初来源是rtc芯片内的时间,来自于系统初始化时获得。

再看kernel源码里面最底层的时间函数:

怎么看容器ip_linux_16

这段代码里面已经与硬件编程相关,再往下就硬件驱动层的代码,我们不再做过多的分析,总之,我们清楚的看到了linux中时间的来自何处,同时也知道了上层应用是如何使用获取时间的syscall。

三、容器中的时间

在之前的分析中,我们知道在linux中获取时间的整个流程,那么我们接下来要回答下为啥容器里面的时间改不得?

这个答案其实很简单,看代码:

怎么看容器ip_系统时间_17

内核中将timekeeper设置为全局变量,所以只要去修改系统时间,这个影响就是内核层面的,所以在docker的实现中默认是禁止在容器内修改时间的,因为容器与虚拟化的区别就在于是否共享内核,这就意味着一旦在容器中修改了时间,这个影响就是全局性的,这个在kubernetes集群中肯定是不允许的。

四、回答之前的问题

在之前分析中,我们提到了copy_to_user这个方法,说到会解释这个方法的巧妙之处,接下来我们来揭晓下这个答案:

怎么看容器ip_Linux怎么查询全部容器时间_18

我们知道,在X86_64体系架构下,除了普通的系统调用外,还提供了sysenter和vsyscall方式来获取内核态的数据,在gettimeofday的实现上,采用的就是vsyscall的方式,即创建了一个共享的内存页面,这个页面是在内核态的,它的数据由内核来维护,但是用户态也有权限访问这个内核页面,因此,不通过中断gettimeofday也可以拿到系统时间,这样就保证这个系统调用的高效性。

以上就是linux时间机制的全部内容,本篇到此就结束了,下一篇介绍如何在容器内部方便快捷地修改应用的时间。