Linux时间子系统3:posix clocks & 时间的种类

   1. clock id

        之前一篇文章在讨论 clock_gettime实现分析的时候,记了一个遗留问题,也就是clock_gettime获取的究竟是什么时间,这篇文章来回答这个问题。

        我们绕过vdso,直接看系统调用分支的内核实现:

SYSCALL_DEFINE2(clock_gettime, const clockid_t, which_clock,
		struct __kernel_timespec __user *, tp)
{
	const struct k_clock *kc = clockid_to_kclock(which_clock);
	struct timespec64 kernel_tp;
	int error;

	if (!kc)
		return -EINVAL;

	error = kc->clock_get_timespec(which_clock, &kernel_tp);

	if (!error && put_timespec64(&kernel_tp, tp))
		error = -EFAULT;

	return error;
}

       

static const struct k_clock *clockid_to_kclock(const clockid_t id)
{
	clockid_t idx = id;

	if (id < 0) {
		return (id & CLOCKFD_MASK) == CLOCKFD ?
			&clock_posix_dynamic : &clock_posix_cpu;
	}

	if (id >= ARRAY_SIZE(posix_clocks))
		return NULL;

	return posix_clocks[array_index_nospec(idx, ARRAY_SIZE(posix_clocks))];
}

        从上面的代码不难发现,首先clockid_to_kclock函数根据传入的clockid进行转换,得到k_clock,再调用k_clock数据结构中的clock_get_timespec来获取具体的时间,那么很显然clock id就代表了系统中的不同的时间,注意是系统中的不同的clock 时间,而不是系统时间。那么上面转换函数中的posix_clocks、clock_posix_dynamic以及clock_posix_cpu就是我们要关注的重点了。

2. k_clock数据结构

        首先,无论是posix_clocks,clock_posix_dynamic还是clock_posix_cpu,都是k_clock数据类型,我们看一下它的数据结构定义:

struct k_clock {
	int	(*clock_getres)(const clockid_t which_clock,
				struct timespec64 *tp);
	int	(*clock_set)(const clockid_t which_clock,
			     const struct timespec64 *tp);
	/* Returns the clock value in the current time namespace. */
	int	(*clock_get_timespec)(const clockid_t which_clock,
				      struct timespec64 *tp);
	/* Returns the clock value in the root time namespace. */
	ktime_t	(*clock_get_ktime)(const clockid_t which_clock);
	int	(*clock_adj)(const clockid_t which_clock, struct __kernel_timex *tx);
	int	(*timer_create)(struct k_itimer *timer);
	int	(*nsleep)(const clockid_t which_clock, int flags,
			  const struct timespec64 *);
	int	(*timer_set)(struct k_itimer *timr, int flags,
			     struct itimerspec64 *new_setting,
			     struct itimerspec64 *old_setting);
	int	(*timer_del)(struct k_itimer *timr);
	void	(*timer_get)(struct k_itimer *timr,
			     struct itimerspec64 *cur_setting);
	void	(*timer_rearm)(struct k_itimer *timr);
	s64	(*timer_forward)(struct k_itimer *timr, ktime_t now);
	ktime_t	(*timer_remaining)(struct k_itimer *timr, ktime_t now);
	int	(*timer_try_to_cancel)(struct k_itimer *timr);
	void	(*timer_arm)(struct k_itimer *timr, ktime_t expires,
			     bool absolute, bool sigev_none);
	void	(*timer_wait_running)(struct k_itimer *timr);
};

clock_getres函数可以获取该clock的时间精度,clock_get和clock_set函数可以分别获取和设定当前的时间,这个时间值是一个绝对时间值(对于时间轴而言,这个绝对时间也是相对的,是相对于该timeline的epoch而言),标记了当前时间点。clock计时有可能是不准确的,例如基于系统晶振的clock。一方面本身晶振的精度有限,时间累积长了会出现较大误差。另外,晶振也会随着使用时间的推移、温度的变化等等因素而导致误差。clock_adj函数允许系统根据外部的精确时间信息对本clock进行调整。nsleep和nsleep_restart这两个成员函数可以让进程sleep一段时间。timer_xxx系列函数是和POSIX interval timer相关,具体会在POSIX timer文档中描述。

(以上内容摘自Linux时间子系统之(五):POSIX Clock)

3. posix_clocks 类型

         posix_clocks数组定义了系统支持的所有的clock,相关的定义如下:

static const struct k_clock * const posix_clocks[] = {
	[CLOCK_REALTIME]		= &clock_realtime,
	[CLOCK_MONOTONIC]		= &clock_monotonic,
	[CLOCK_PROCESS_CPUTIME_ID]	= &clock_process,
	[CLOCK_THREAD_CPUTIME_ID]	= &clock_thread,
	[CLOCK_MONOTONIC_RAW]		= &clock_monotonic_raw,
	[CLOCK_REALTIME_COARSE]		= &clock_realtime_coarse,
	[CLOCK_MONOTONIC_COARSE]	= &clock_monotonic_coarse,
	[CLOCK_BOOTTIME]		= &clock_boottime,
	[CLOCK_REALTIME_ALARM]		= &alarm_clock,
	[CLOCK_BOOTTIME_ALARM]		= &alarm_clock,
	[CLOCK_TAI]			= &clock_tai,
};

        POSIX标准定义了4种类型的clock,CLOCK_REALTIME、CLOCK_MONOTONIC、CLOCK_PROCESS_CPUTIME_ID和CLOCK_THREAD_CPUTIME_ID,其他是linux specific。

        关于这几种clock的含义,如下(引用自Linux时间子系统之(三):用户空间接口函数):

       CLOCK_REALTIME:系统时钟,它表示了真实世界的墙上时钟(gettimeofday没有指定CLOCK ID,实际上获取的就是CLOCK_REALTIME的时间值)。CLOCK_REALTIME这个系统时钟允许用户对其进行设定(当然要有CAP_SYS_TIME权限),这也就表示在用户空间可以对该系统时钟进行修改,产生不连续的时间间断点。除此之外,也可以通过NTP对该时钟进行调整(不会有间断点,NTP调整的是local oscillator和上游服务器频率误差而已)。

        CLOCK_MONOTONIC:该时钟也是真实世界的墙上时钟,只不过其基准点不一定是linux epoch(当然也可以是),一般会把系统启动的时间点设定为其基准点。随后该时钟会不断的递增。除了可以通过NTP对该时钟进行调整之外,其他任何程序不允许设定该时钟,这样也就保证了该时钟的单调性。

        CLOCK_MONOTONIC_RAW具备CLOCK_MONOTONIC的特性,除了NTP调整。也就是说,clock id是CLOCK_MONOTONIC_RAW的系统时钟是一个完全基于本地晶振的时钟。不能设定,也不能对对晶振频率进行调整

        除了CLOCK_REALTIME,为何要有其他类型的系统时钟呢?MONOTONIC类型的时钟相对比较简单,如果你设定事件A之后5秒进行动作B,那么用MONOTONIC类型的时钟是一个比较好的选择,如果使用REALTIME的时钟,当用户在事件A和动作B之间插入时间设定的操作,那么你设定事件A之后5秒进行动作B将不能触发。此外,用户需要了解系统启动时间,这个需求需要使用MONOTONIC类型的时钟的时钟。需要指出的是MONOTONIC类型的时钟不是绝对时间的概念,多半是计算两个采样点之间的时间,并且保证采样点之间时间的单调性。MONOTONIC_RAW是一个底层工具,一般而言程序员不会操作它,使用MONOTONIC类型的时钟就够用了,当然,一些高级的应用场合,例如你想使用另外的方法(不是NTP)来调整时间,那么就可以使用MONOTONIC_RAW了。

        有些应用场景使用real time的时钟(墙上时钟)是不合适的,例如当我们进行系统中各个应用程序的性能分析和统计的时候。正因为如此,kernel提供了基于进程或者线程的系统时钟,也就是CLOCK_PROCESS_CPUTIME_ID和CLOCK_THREAD_CPUTIME_ID了。这两个clock是专门用来计算进程或者线程的执行时间的(用于性能剖析),一旦进程(线程)被切换出去,那么该进程(线程)的clock就会停下来。因此,这两种的clock都是per-process或者per-thread的,而其他的clock都是系统级别的。

4. 各种posix_clocks 的定义

        Linux时间子系统之(五):POSIX Clock这篇文章已经讲得很好了,实现也都比较简单,我们记住一点,从代码看,以上各类clock提供的时间都和timekeeper相关,也就是时钟源都是系统时间的时钟源

        我们单独看一下clock_posix_cpu,在posix_clocks数组中并没有包含clock_posix_cpu,但是posix_clock中的CLOCK_PROCESS_CPUTIME_ID,CLOCK_THREAD_CPUTIME_ID对应的clock_process和clock_thread中的函数却是直接调用了clock_posix_cpu中的操作接口。至于具体的实现就不讲了,总之它的时钟源与系统时钟的时钟源是一致的。

5. clock_posix_dynamic

        有些硬件提供了clock的能力,不适合静态定义,Linux里面用clock_posix_dynamic数据结构来对接这些动态的clock,很明显这类clock的时钟源与系统时钟源就不一样了。

        那么我们首先看一下clock_posix_dynamic数据结构

const struct k_clock clock_posix_dynamic = {
	.clock_getres		= pc_clock_getres,
	.clock_set		= pc_clock_settime,
	.clock_get_timespec	= pc_clock_gettime,
	.clock_adj		= pc_clock_adjtime,
};
static int pc_clock_gettime(clockid_t id, struct timespec64 *ts)
{
	struct posix_clock_desc cd;
	int err;

	err = get_clock_desc(id, &cd);
	if (err)
		return err;

	if (cd.clk->ops.clock_gettime)
		err = cd.clk->ops.clock_gettime(cd.clk, ts);
	else
		err = -EOPNOTSUPP;

	put_clock_desc(&cd);

	return err;
}

        它实际上是所有dynamic时钟的入口,通过它的gettime函数,我们可以看到,它需要根据传入的clock id去寻找具体的时钟,再调用对应时钟的clock_gettime函数。那么dynamic时钟是如何注册的呢?

        linux提供了posix_clock_register用于注册dynamic时钟,该函数的具体实现就不讲了。我们看一个具体的应用案例,注册ptp硬件时钟的时候,就调用了这个函数

	ptp->dev.devt = ptp->devid;
	ptp->dev.class = ptp_class;
	ptp->dev.parent = parent;
	ptp->dev.groups = ptp->pin_attr_groups;
	ptp->dev.release = ptp_clock_release;
	dev_set_drvdata(&ptp->dev, ptp);
	dev_set_name(&ptp->dev, "ptp%d", ptp->index);

	/* Create a posix clock and link it to the device. */
	err = posix_clock_register(&ptp->clock, &ptp->dev);
	if (err) {
		pr_err("failed to create posix clock\n");
		goto no_clock;
	}

        很显然,作为clock,硬件需要实现posix_clock_operations定义的时钟操作。用户态又是如何获取动态时钟的时间的呢?用户在注册ptp时钟后,会生成/dev/ptp设备,通过打开设备获取到clock id,就可以通过用户态接口clock_gettime获取到注册的硬件时间。

clockid_t phc_open(char *phc)
{
	clockid_t clkid;
	struct ptp_clock_caps caps;
	int fd = open(phc, O_RDWR);

	if (fd < 0)
		return CLOCK_INVALID;

	clkid = FD_TO_CLOCKID(fd);
	/* check if clkid is valid */
	if (phc_get_caps(clkid, &caps)) {
		close(fd);
		return CLOCK_INVALID;
	}

	return clkid;
}

  6. 总结

        通过上面的分析,我们知道,在Linux中有各种种类的时钟,应用需要根据不同的情况使用不同的时间。同时,这些时钟的时钟源也不完全相同,关于时钟源的部分以及不同的时钟源如何同步,我们后面再进行分析。

        

  • 27
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Bluetangos

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值