浅谈时间函数gettimeofday的成本 ——gettimeofday是由底层do_gettimeofday返回的?!

本文解析了gettimeofday函数的工作原理,包括其在不同体系架构上的实现差异、时间精度的维持机制及调用成本。同时探讨了jiffies变量的并发访问与周期回转问题。
转自

       我们在程序中会频繁地取当前时间,例如处理一个http请求时,两次调用gettimeofday取差值计算出处理该请求消耗了多少秒。这样的调用无处不在,所以我们有必要详细了解下,gettimeofday这个函数做了些什么?内核1ms一次的时钟中断处理真的可以支持tv_usec字段达到微秒精度吗?它的调用成本在i386/x86_64体系架构上代价一样吗?如果在系统繁忙时,频繁的调用它有问题吗?


gettimeofday是C库提供的函数(不是系统调用),它封装了内核里的sys_gettimeofday系统调用,就是说,归根到底是系统调用。


但是,内核对于x86_64体系结构下,除了普通的系统调用外,还提供了sysenter和vsyscall方式来获取内核态的数据。目前我们使用的操作系统大都是x86_64体系的,如果我们用strace命令跟踪,就会发现gettimeofday命令实际上没有执行系统调用(i386体系会有),这是因为:x86_64体系上,使用vsyscall实现了gettimeofday这个系统调用。具体就是,创建了一个共享的内存页面,它是在内核态的,它的数据由内核来维护,但是,用户态也有权限访问这个内核页面,由此,不通过中断gettimeofday也就拿到了系统时间。


接下来,我来详细回答以上4个问题。


一、gettimeofday做了些什么?

它把内核保存的墙上时间和jiffies综合处理后返回给用户。解释下墙上时间和jiffies是什么:1、墙上时间就是实际时间(1970/1/1号以来的时间),它是由我们主板电池供电的(装过PC机的同学都了解)RTC单元存储的,这样即使机器断电了时间也不用重设。当操作系统启动时,会用这个RTC来初始化墙上时间,接着,内核会在一定精度内根据jiffies维护这个墙上时间。2、jiffies就是操作系统启动后经过的时间,它的单位是节拍数。有些体系架构,1个节拍数是10ms,但我们常用的x86体系下,1个节拍数是1ms。也就是说,jiffies这个全局变量存储了操作系统启动以来共经历了多少毫秒。我们来看看gettimeofday是如何做的。首先它调用了sys_gettimeofday系统调用。

  1. asmlinkage long sys_gettimeofday(struct timeval __user *tv, struct timezone __user *tz)  
  2. {  
  3.     if (likely(tv != NULL)) {  
  4.         struct timeval ktv;  
  5.         do_gettimeofday(&ktv);  
  6.         if (copy_to_user(tv, &ktv, sizeof(ktv)))  
  7.             return -EFAULT;  
  8.     }  
  9.     if (unlikely(tz != NULL)) {  
  10.         if (copy_to_user(tz, &sys_tz, sizeof(sys_tz)))  
  11.             return -EFAULT;  
  12.     }  
  13.     return 0;  
  14. }  
asmlinkage long sys_gettimeofday(struct timeval __user *tv, struct timezone __user *tz)
{
	if (likely(tv != NULL)) {
		struct timeval ktv;
		do_gettimeofday(&ktv);
		if (copy_to_user(tv, &ktv, sizeof(ktv)))
			return -EFAULT;
	}
	if (unlikely(tz != NULL)) {
		if (copy_to_user(tz, &sys_tz, sizeof(sys_tz)))
			return -EFAULT;
	}
	return 0;
}

大家看到,它调用do_gettimeofday函数取到当前时间存储到局部变量ktv上,然后调用copy_to_user把结果复制到用户空间。每个体系都有自己的实现,我这里就简单列下x86_64体系下do_gettimeofday的实现:

  1. void do_gettimeofday(struct timeval *tv)  
  2. {  
  3.     unsigned long seq, t;  
  4.     unsigned int sec, usec;  
  5.   
  6.     do {  
  7.         seq = read_seqbegin(&xtime_lock);  
  8.   
  9.         sec = xtime.tv_sec;  
  10.         usec = xtime.tv_nsec / 1000;  
  11.   
  12.         /* i386 does some correction here to keep the clock  
  13.            monotonous even when ntpd is fixing drift. 
  14.            But they didn't work for me, there is a non monotonic 
  15.            clock anyways with ntp. 
  16.            I dropped all corrections now until a real solution can 
  17.            be found. Note when you fix it here you need to do the same 
  18.            in arch/x86_64/kernel/vsyscall.c and export all needed 
  19.            variables in vmlinux.lds. -AK */   
  20.   
  21.         t = (jiffies - wall_jiffies) * (1000000L / HZ) +  
  22.             do_gettimeoffset();  
  23.         usec += t;  
  24.   
  25.     } while (read_seqretry(&xtime_lock, seq));  
  26.   
  27.     tv->tv_sec = sec + usec / 1000000;  
  28.     tv->tv_usec = usec % 1000000;  
  29. }  
void do_gettimeofday(struct timeval *tv)
{
	unsigned long seq, t;
 	unsigned int sec, usec;

	do {
		seq = read_seqbegin(&xtime_lock);

		sec = xtime.tv_sec;
		usec = xtime.tv_nsec / 1000;

		/* i386 does some correction here to keep the clock 
		   monotonous even when ntpd is fixing drift.
		   But they didn't work for me, there is a non monotonic
		   clock anyways with ntp.
		   I dropped all corrections now until a real solution can
		   be found. Note when you fix it here you need to do the same
		   in arch/x86_64/kernel/vsyscall.c and export all needed
		   variables in vmlinux.lds. -AK */ 

		t = (jiffies - wall_jiffies) * (1000000L / HZ) +
			do_gettimeoffset();
		usec += t;

	} while (read_seqretry(&xtime_lock, seq));

	tv->tv_sec = sec + usec / 1000000;
	tv->tv_usec = usec % 1000000;
}

大家看到,只是把xtime加以jiffies修正后返回给用户而已。而xtime变量和jiffies的维护更新频率,就决定了时间精度,上面说了,每10或者1ms才处理一次时钟中断,难道精度只到1ms吗?继续往下。


二、内核1ms一次的时钟中断真的可以支持tv_usec字段达到微秒精度吗?
可以,因为这个时间还会由High Precision Event Timer来维护,这个模块会处理微秒级的中断,并更新xtime和jiffies变量。我们看下x86_64体系结构下的维护代码:

  1. static struct irqaction irq0 = {  
  2.     timer_interrupt, SA_INTERRUPT, CPU_MASK_NONE, "timer", NULL, NULL  
  3. };  
static struct irqaction irq0 = {
	timer_interrupt, SA_INTERRUPT, CPU_MASK_NONE, "timer", NULL, NULL
};

这个timer_interrupt函数会处理HPET时间中断,来更新xtime变量。


三、它的调用成本在所有的操作系统上代价一样吗?如果在系统繁忙时,1毫秒内调用多次有问题吗?

最上面已经说了,对于x86_64系统来说,这是个虚拟系统调用vsyscall!所以,这里它不用发送中断!速度很快,成本低,调用一次的成本大概不到一微秒!

对于i386体系来说,这就是系统调用了!最简单的系统调用都有无法避免的成本:陷入内核态。当我们调用gettimeofday时,将会向内核发送软中断,然后将陷入内核态,这时内核至少要做下列事:处理软中断、保存所有寄存器值、从用户态复制函数参数到内核态、执行、将结果复制到用户态。这些成本至少在1微秒以上!


四、关于jiffies值得一提的两点

先看看它的定义:

  1. volatile unsigned long __jiffies;  
volatile unsigned long __jiffies;

只谈两点。

1、它用了一个C语言里比较罕见的关键字volatile,这个关键字用于解决并发问题。C语言编译器很喜欢做优化的,它不清楚某个变量可能会被并发的修改,例如上面的jiffies变量首先是0,如果首先一个CPU修改了它的值为1,紧接着另一个CPU在读它的值,例如 __jiffies = 0; while (__jiffies == 1),那么在内核的C代码中,如果不加volatile字段,那么第二个CPU里的循环体可能不会被执行到,因为C编译器在对代码做优化时,生成的汇编代码不一定每次都会去读内存!它会根据代码把变量__jiffies设为0,并一直使用下去!而加了volatile字段后,就会要求编译器,每次使用到__jiffies时,都要到内存里真实的读取这个值。


2、它的类型是unsigned long,在32位系统中,最大值也只有43亿不到,从系统启动后49天就到达最大值了,之后就会清0重新开始。那么jiffies达到最大值时的回转问题是怎么解决的呢?或者换句话说,我们需要保证当jiffies回转为一个小的正数时,例如1,要比几十秒毫秒前的大正数大,例如4294967290,要达到jiffies(1)>jiffies(4294967290)这种效果。

内核是通过定义了两个宏来解决的:

  1. #define time_after(a,b)     \  
  2.     (typecheck(unsigned long, a) && \  
  3.      typecheck(unsigned long, b) && \  
  4.      ((long)(b) - (long)(a) < 0))  
  5. #define time_before(a,b)    time_after(b,a)  
#define time_after(a,b)		\
	(typecheck(unsigned long, a) && \
	 typecheck(unsigned long, b) && \
	 ((long)(b) - (long)(a) < 0))
#define time_before(a,b)	time_after(b,a)

很巧妙的设计!仅仅把unsigned long转为long类型后相减比较,就达到了jiffies(1)>jiffies(4294967290)效果,简单的解决了jiffies的回转问题,赞一个。


<think> 首先,用户的问题是:我想了解内核时间函数与用户空间时间函数(如gettimeofday)的区别。我需要回答内核时间函数和用户空间时间函数(如gettimeofday)的区别。 从系统指令中,我必须: - 使用$...$格式用于行内数学表达式。 - 使用$$...$$格式用于独立公式。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 在回答中引用的段落末尾自然地添加引用标识,例如[^1]。 用户提供了几个引用,我需要参考它们: - 引用[1]、[2]、[3]、[4]都是关于Linux内核中的时间函数,特别是do_gettimeofday。 用户上次的问题是关于内核时间函数,但在这个对话中,用户的问题是新的:[用户可见层]部分是用户的问题。系统指令说"参考站内引用",但这些引用似乎是假的或示例性的;它们是关于Linux内核时间函数的。 用户说:"上述引用仅供参考,并不是用户真正的需求,请尽可能多的参考用户上一次的问题和你的回答。" 但在对话历史中,用户上一次的问题是关于内核时间函数与用户空间时间函数的区别。 在上下文: - 第一个消息是系统指令。 - 第二个消息是用户提供引用和自己的问题。 - 用户的问题:"我想了解内核时间函数与用户空间时间函数(如gettimeofday)的区别" 所以我需要专注于回答这个区别。 关键点: - 内核时间函数:如do_gettimeofday,用于内核空间获取时间。 - 用户空间时间函数:如gettimeofday,系统调用,用户程序使用它。 区别包括: 1. **空间**:内核函数在内核空间运行,用户函数在用户空间调用系统调用。 2. **访问权限**:内核函数可以直接访问硬件时钟或内核数据结构,用户函数需要通过系统调用进入内核。 3. **精度和效率**:内核函数可能更高精度,因为避免上下文切换;用户函数有开销。 4. **用途**:内核函数用于内核模块、驱动程序;用户函数用于应用程序。 5. **数据类型**:两者都使用struct timeval或类似,但获取方式不同。 从引用: - 引用[1]和[3]提到do_gettimeofday在内核中填充struct timeval。 - 引用[2]提到系统时间和实时时间,jiffies和xtime。 - 引用[4]说处理墙上时钟时间最好留在用户空间。 结构回答: - 介绍区别。 - 解释内核时间函数(如do_gettimeofday)。 - 解释用户空间函数(如gettimeofday)。 - 比较区别。 - 添加引用标识自然地。 - 生成相关问题。 使用中文回答。 生成相关问题:类似于示例,在回答后添加"§§相关问题§§"然后列出问题。 保证回答真实可靠:基于Linux内核知识。 大纲回答: 1. **引言**:简要介绍内核和用户空间时间函数的区别。 2. **内核时间函数**:解释如do_gettimeofday,如何使用。 3. **用户空间时间函数**:解释如gettimeofday,如何工作。 4. **主要区别**:列出关键区别点。 - 执行环境 - 访问方式 - 精度 - 使用场景 5. **结论**:总结。 6. **相关问题**。 添加引用:例如,当提到do_gettimeofday时添加[^1],提到jiffies时添加[^2]。 在回答中自然地添加引用标识。 </think> ### 内核时间函数与用户空间时间函数(如gettimeofday)的区别 在 Linux 系统中,时间管理分为内核空间和用户空间两个层次,两者的时间函数在实现、访问方式和应用场景上有显著差异。以下我将基于 Linux 内核和用户空间的工作原理,逐步解释这些区别。核心区别在于:内核时间函数直接操作硬件或内核数据结构,效率高但仅限内核使用;而用户空间时间函数(如 `gettimeofday`)需要通过系统调用进入内核,引入开销但适用于应用程序。以下是详细分析。 #### 1. **内核时间函数(如 `do_gettimeofday`)** - **功能与访问方式**:内核时间函数在内核空间直接执行,无需系统调用开销。例如,`do_gettimeofday` 函数(定义在 `<linux/time.h>`)直接在驱动或内核模块中使用,填充一个 `struct timeval` 结构体,包含秒和微秒值(例如当前时间戳)。这避免了上下文切换,提高了精度(可达微秒级)[^1][^3]。 - **数据类型**:内核函数通常处理原始时间数据,如 `struct timeval` 或内核全局变量(例如 `jiffies` 表示系统启动后的时钟滴答数,相对时间;`xtime` 表示实时时钟值,绝对时间)。这些数据可直接访问硬件时钟(如 TSC 或 RTC)[^2][^4]。 - **使用场景**:主要用于内核模块、驱动程序或中断处理中获取高精度时间,例如测量硬件事件间隔或调度延迟。但内核函数不适合处理人可读时间(如年月日),因为这会增加策略相关性,建议交由用户空间处理[^4][^3]。 - **示例**:在驱动代码中调用 `do_gettimeofday` 获取当前时间戳: ```c #include <linux/time.h> struct timeval tv; do_gettimeofday(&tv); // 填充 tv 结构体 ``` #### 2. **用户空间时间函数(如 `gettimeofday`)** - **功能与访问方式**:用户空间函数在应用程序中使用,通过系统调用(syscall)间接访问内核时间数据。例如,`gettimeofday` 是一个系统调用,用户程序调用它时,会触发上下文切换到内核模式,由内核的 `do_gettimeofday` 或类似函数处理后返回 `struct timeval` 值给用户空间[^1][^3]。这引入了开销(微秒级延迟),但提供了标准化接口。 - **数据类型**:同样使用 `struct timeval`,但用户空间函数通常封装为更高级的表示,如 `tm` 结构(年月日时分秒),支持时区转换和人可读格式(通过 C 库函数如 `localtime`)[^4]。 - **使用场景**:适用于应用程序需要墙上时钟时间(wall-clock time),例如日志记录、定时任务(cron)或性能分析。用户空间库(如 glibc)提供丰富的时间处理函数,简化了开发[^4]。 - **示例**:在用户程序中调用 `gettimeofday`: ```c #include <sys/time.h> struct timeval tv; gettimeofday(&tv, NULL); // 系统调用返回当前时间 ``` #### 3. **主要区别总结** | 方面 | 内核时间函数(如 `do_gettimeofday`) | 用户空间时间函数(如 `gettimeofday`) | |------|--------------------------------------|----------------------------------------| | **执行环境** | 在内核空间直接运行,无上下文切换开销 | 在用户空间发起系统调用,需切换上下文,引入延迟 | | **访问方式** | 直接访问硬件时钟或内核数据结构(如 `jiffies`、`xtime`) | 间接通过系统调用访问内核数据,依赖内核接口[^1][^2] | | **精度** | 更高(微秒级),适合低延迟场景 | 相对较低(受系统调用开销影响),但通常足够应用需求 | | **数据类型** | 处理原始时间值(如 `struct timeval`),不处理人可读格式 | 支持人可读转换(如 `struct tm`),通过 C 库实现 | | **使用场景** | 内核模块、驱动程序、实时性要求高的任务 | 用户应用程序、日志系统、定时作业 | | **安全性与策略** | 更底层,需谨慎使用以避免内核崩溃 | 更安全,由用户空间库处理错误和策略(如时区)[^4] | 总体而言,内核时间函数追求效率和直接性,而用户空间时间函数强调易用性和可移植性。例如,`gettimeofday` 在用户空间是对内核 `do_gettimeofday` 的封装,但增加了系统调用层,导致性能差异(在内核中调用 `do_gettimeofday` 可能比用户空间 `gettimeofday` 快 10-100 倍)[^1][^3]。 #### 4. **为什么区分?** - **效率考虑**:内核函数避免不必要的开销,适合实时系统。 - **设计原则**:Linux 内核鼓励将人可读时间处理留在用户空间,减少内核复杂性。内核只提供原始时间数据,应用程序负责格式化[^4][^3]。 - **兼容性**:用户空间函数(如 `clock_gettime`)支持多种时钟源(如 `CLOCK_REALTIME`、`CLOCK_MONOTONIC`),而内核函数更专注于底层机制。 通过理解这些区别,开发者可以选择合适的时间函数:在内核开发时使用 `do_gettimeofday` 获取高精度时间;在应用程序中使用 `gettimeofday` 或更现代的 `clock_gettime` 满足通用需求。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值