Linux中计时相关函数的实现
一、时间比较宏time_after
#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)
#define time_after_eq(a,b) \(typecheck(unsigned long, a) && \typecheck(unsigned long, b) && \((long)(a) - (long)(b) >= 0))
#define time_before_eq(a,b) time_after_eq(b,a)
1. 宏定义分析
1.1. time_after(a,b)
宏
第一行:
(typecheck(unsigned long, a) &&
typecheck(unsigned long, a)
是类型检查宏- 确保参数
a
是unsigned long
类型 - 如果不是该类型,编译时会报错
&&
表示逻辑与,所有条件都必须满足
第二行:
typecheck(unsigned long, b) &&
- 同样对参数
b
进行类型检查 - 确保
b
也是unsigned long
类型
第三行:
((long)(b) - (long)(a) < 0)
这是核心的逻辑部分:
- 将两个
unsigned long
参数强制转换为long
类型,进行有符号运算 - 计算
(long)(b) - (long)(a)
- 检查结果是否小于 0
1.2. time_before(a,b)
宏
#define time_before(a,b) time_after(b,a)
- 简单地调换参数顺序调用
time_after
1.3. time_after_eq
宏
- 和上述类似
二、32位系统获取64位jiffies函数get_jiffies_64
u64 get_jiffies_64(void)
{unsigned long seq;u64 ret;do {seq = read_seqbegin(&xtime_lock);ret = jiffies_64;} while (read_seqretry(&xtime_lock, seq));return ret;
}
函数整体理解
u64 get_jiffies_64(void)
- 返回:
u64
(64位无符号整数) - 作用:安全地读取64位jiffies计数器,避免在32位系统上读取时发生数据不一致
变量声明
unsigned long seq;u64 ret;
seq
:顺序锁的序列号,用于检测读期间是否有写操作发生ret
:保存读取到的jiffies_64值
核心循环机制
do {seq = read_seqbegin(&xtime_lock);ret = jiffies_64;} while (read_seqretry(&xtime_lock, seq));
这是一个读-验证-重试的循环,确保读取到一致的数据
步骤1:开始读取序列
seq = read_seqbegin(&xtime_lock);
read_seqbegin()
:读取顺序锁的当前序列号- 如果序列号是偶数,表示没有写操作在进行,可以安全读取
- 如果序列号是奇数,表示有写操作正在进行,会自旋等待直到写操作完成
步骤2:读取数据
ret = jiffies_64;
- 读取64位jiffies值到局部变量
ret
中 - 在32位系统上,这需要两次32位内存读取(高32位和低32位)
步骤3:验证数据一致性
while (read_seqretry(&xtime_lock, seq));
read_seqretry()
:检查序列号是否发生了变化- 如果序列号与开始时相同,说明读取期间没有写操作,数据一致
- 如果序列号变化了,说明在我们读取过程中有写操作发生,需要重新读取
三、jiffies时间和其他表示法转换timespec_to_jiffies
/* TICK_NSEC is the time between ticks in nsec assuming real ACTHZ */
#define TICK_NSEC (SH_DIV (1000000UL * 1000, ACTHZ, 8))
#define MAX_JIFFY_OFFSET ((~0UL >> 1)-1)
#define NSEC_PER_SEC (1000000000L)
# define MAX_SEC_IN_JIFFIES \(long)((u64)((u64)MAX_JIFFY_OFFSET * TICK_NSEC) / NSEC_PER_SEC)
#define SEC_CONVERSION ((unsigned long)((((u64)NSEC_PER_SEC << SEC_JIFFIE_SC) +\TICK_NSEC -1) / (u64)TICK_NSEC))
#if HZ >= 12 && HZ < 24
# define SHIFT_HZ 4
#elif HZ >= 24 && HZ < 48
# define SHIFT_HZ 5
#elif HZ >= 48 && HZ < 96
# define SHIFT_HZ 6
#elif HZ >= 96 && HZ < 192
# define SHIFT_HZ 7
#elif HZ >= 192 && HZ < 384
# define SHIFT_HZ 8
#elif HZ >= 384 && HZ < 768
# define SHIFT_HZ 9
#elif HZ >= 768 && HZ < 1536
# define SHIFT_HZ 10
#else
# error You lose.
#endif
#define SEC_JIFFIE_SC (31 - SHIFT_HZ)
#define SH_DIV(NOM,DEN,LSH) ( ((NOM / DEN) << LSH) \+ (((NOM % DEN) << LSH) + DEN / 2) / DEN)
#define LATCH ((CLOCK_TICK_RATE + HZ/2) / HZ) /* For divider */
/* HZ is the requested value. ACTHZ is actual HZ ("<< 8" is for accuracy) */
#define ACTHZ (SH_DIV (CLOCK_TICK_RATE, LATCH, 8))
static __inline__ unsigned long
timespec_to_jiffies(const struct timespec *value)
{unsigned long sec = value->tv_sec;long nsec = value->tv_nsec + TICK_NSEC - 1;if (sec >= MAX_SEC_IN_JIFFIES){sec = MAX_SEC_IN_JIFFIES;nsec = 0;}return (((u64)sec * SEC_CONVERSION) +(((u64)nsec * NSEC_CONVERSION) >>(NSEC_JIFFIE_SC - SEC_JIFFIE_SC))) >> SEC_JIFFIE_SC;}
1. 基础常量定义
#define TICK_NSEC (SH_DIV (1000000UL * 1000, ACTHZ, 8))
- 计算每个tick的纳秒数
1000000UL * 1000
= 1,000,000,000(10亿,即1秒的纳秒数)ACTHZ
是实际的系统时钟频率- 公式:
TICK_NSEC = NSEC_PER_SEC / ACTHZ
- 使用
SH_DIV
进行高精度除法
#define NSEC_PER_SEC (1000000000L)
- 1秒 = 10亿纳秒
2. 最大限制定义
#define MAX_JIFFY_OFFSET ((~0UL >> 1)-1)
~0UL
是所有位为1的无符号长整数>> 1
右移一位得到最大有符号正数-1
再减1,为计算留出安全余量- 例如32位:
0xFFFFFFFF → 0x7FFFFFFF → 0x7FFFFFFE
#define MAX_SEC_IN_JIFFIES \(long)((u64)((u64)MAX_JIFFY_OFFSET * TICK_NSEC) / NSEC_PER_SEC)
- 计算jiffies能表示的最大秒数
- 公式:
MAX_SEC = (MAX_JIFFY_OFFSET × TICK_NSEC) / NSEC_PER_SEC
- 使用u64避免乘法溢出
3. 系统时钟频率相关
#if HZ >= 12 && HZ < 24
# define SHIFT_HZ 4
// ... 其他范围
#endif
- 根据HZ值选择最优的移位值
- 用于后续的缩放计算优化
#define SEC_JIFFIE_SC (31 - SHIFT_HZ)
- 秒到jiffies的缩放因子
4. 转换常数定义
#define SEC_CONVERSION ((unsigned long)((((u64)NSEC_PER_SEC << SEC_JIFFIE_SC) +\TICK_NSEC -1) / (u64)TICK_NSEC))
这是最关键的预计算常数:
(u64)NSEC_PER_SEC << SEC_JIFFIE_SC
:将1秒的纳秒数左移缩放+ TICK_NSEC - 1
:向上取整,确保不会因为截断而少算/ (u64)TICK_NSEC
:除以每个tick的纳秒数- 结果:缩放后的"每秒对应的jiffies数"
实际意义:
SEC_CONVERSION = ceil( (NSEC_PER_SEC << SEC_JIFFIE_SC) / TICK_NSEC )
5. timespec_to_jiffies
函数详解
static __inline__ unsigned long
timespec_to_jiffies(const struct timespec *value)
{unsigned long sec = value->tv_sec;long nsec = value->tv_nsec + TICK_NSEC - 1;
- 提取秒和纳秒部分
+ TICK_NSEC - 1
:纳秒部分向上取整,确保足够的纳秒数会计入一个jiffy
if (sec >= MAX_SEC_IN_JIFFIES){sec = MAX_SEC_IN_JIFFIES;nsec = 0;}
- 溢出保护:如果超过最大可表示时间,截断到最大值
return (((u64)sec * SEC_CONVERSION) +(((u64)nsec * NSEC_CONVERSION) >>(NSEC_JIFFIE_SC - SEC_JIFFIE_SC))) >> SEC_JIFFIE_SC;
}
5.1. 秒部分转换
(u64)sec * SEC_CONVERSION
SEC_CONVERSION 的含义:
#define SEC_CONVERSION ((NSEC_PER_SEC << SEC_JIFFIE_SC) / TICK_NSEC)
实际意义:
NSEC_PER_SEC << SEC_JIFFIE_SC
:将1秒的纳秒数放大 2^SEC_JIFFIE_SC 倍/ TICK_NSEC
:除以每个tick的纳秒数- 结果:每秒钟对应的放大后的jiffies数
5.2. 纳秒部分转换
((u64)nsec * NSEC_CONVERSION) >> (NSEC_JIFFIE_SC - SEC_JIFFIE_SC)
NSEC_CONVERSION 的含义:
#define NSEC_CONVERSION ((1 << NSEC_JIFFIE_SC) / TICK_NSEC)
实际意义:
1 << NSEC_JIFFIE_SC
:将1纳秒放大 2^NSEC_JIFFIE_SC 倍/ TICK_NSEC
:除以每个tick的纳秒数- 结果:每纳秒对应的放大后的jiffies数
缩放因子调整:
>> (NSEC_JIFFIE_SC - SEC_JIFFIE_SC)
因为:
NSEC_CONVERSION
使用NSEC_JIFFIE_SC
缩放SEC_CONVERSION
使用SEC_JIFFIE_SC
缩放- 需要将纳秒部分的缩放调整到与秒部分相同的级别
5.3. 最终结果处理
>> SEC_JIFFIE_SC
作用: 去除缩放因子,得到真实的jiffies值
6. 设计原理深度解析
// 直接计算(可能溢出和精度丢失)
jiffies = (sec * NSEC_PER_SEC + nsec) / TICK_NSEC;
缩放方法的优势:
- 避免除法:使用预计算的常数,运行时只有乘法和移位,内核一般不允许用浮点运算
- 保持高精度:通过放大因子保留小数部分
- 防止溢出:合理选择缩放因子控制中间结果大小
缩放因子选择
#define SEC_JIFFIE_SC (31 - SHIFT_HZ)
#define NSEC_JIFFIE_SC (SEC_JIFFIE_SC + 29)
为什么是 +29?
29 = log2(1,000,000,000)
的近似值- 因为纳秒到秒有109倍关系,229 接近这个数量级
四、jiffies时间和其他表示法转换jiffies_to_timespec
#define div_long_long_rem(dividend,divisor,remainder) \
({ \u64 result = dividend; \*remainder = do_div(result,divisor); \result; \
})
static __inline__ void
jiffies_to_timespec(const unsigned long jiffies, struct timespec *value)
{/** Convert jiffies to nanoseconds and separate with* one divide.*/u64 nsec = (u64)jiffies * TICK_NSEC;value->tv_sec = div_long_long_rem(nsec, NSEC_PER_SEC, &value->tv_nsec);
}
1. div_long_long_rem 宏定义
#define div_long_long_rem(dividend,divisor,remainder) \
({ \u64 result = dividend; \*remainder = do_div(result,divisor); \result; \
})
这是一个同时返回商和余数的64位除法宏
u64 result = dividend;
- 将被除数保存到临时变量
result
中
*remainder = do_div(result,divisor);
do_div(result, divisor)
:执行64位除法- 输入:
result
作为被除数,divisor
作为除数 - 操作:
result = result / divisor
,同时返回余数 - 输出:余数存入
*remainder
,商存入result
- 输入:
result;
- 返回商(
result
现在包含除法结果)
2. jiffies_to_timespec
函数
static __inline__ void
jiffies_to_timespec(const unsigned long jiffies, struct timespec *value)
函数作用: 将jiffies转换为timespec
结构体(秒 + 纳秒)
步骤1:计算总纳秒数
u64 nsec = (u64)jiffies * TICK_NSEC;
(u64)jiffies
:将jiffies转换为64位,避免乘法溢出TICK_NSEC
:每个tick的纳秒数- 结果:
nsec
= 总纳秒数
步骤2:分离秒和纳秒部分
value->tv_sec = div_long_long_rem(nsec, NSEC_PER_SEC, &value->tv_nsec);
参数:
nsec
:总纳秒数(被除数)NSEC_PER_SEC
:1秒的纳秒数 = 1,000,000,000(除数)&value->tv_nsec
:余数(纳秒部分)的存储地址
执行过程:
- 计算
nsec / NSEC_PER_SEC
得到秒数 - 计算
nsec % NSEC_PER_SEC
得到纳秒部分 - 秒数存入
value->tv_sec
- 纳秒部分存入
value->tv_nsec
五、返回时间戳计数器get_cycles
typedef unsigned long long cycles_t;static inline cycles_t get_cycles (void)
{unsigned long long ret=0;#ifndef CONFIG_X86_TSCif (!cpu_has_tsc)return 0;
#endif#if defined(CONFIG_X86_GENERIC) || defined(CONFIG_X86_TSC)rdtscll(ret);
#endifreturn ret;
}
第一部分:检查TSC支持
#ifndef CONFIG_X86_TSCif (!cpu_has_tsc)return 0;
#endif
条件: #ifndef CONFIG_X86_TSC
- 如果内核配置没有启用TSC支持
执行逻辑:
if (!cpu_has_tsc)return 0;
cpu_has_tsc
:CPU特性检测变量,检查当前CPU是否支持TSC- 如果CPU不支持TSC,直接返回0
第二部分:实际读取TSC
#if defined(CONFIG_X86_GENERIC) || defined(CONFIG_XSC_TSC)rdtscll(ret);
#endif
条件: defined(CONFIG_X86_GENERIC) || defined(CONFIG_X86_TSC)
- 如果配置了通用x86支持或明确启用了TSC支持
执行:
rdtscll(ret);
rdtscll
:内联汇编宏,执行RDTSC
指令读取时间戳计数器- 结果存入
ret
变量
return ret;
- 返回读取到的时间戳计数器值
六、将日期时间转换为时间戳mktime
static inline unsigned long
mktime (unsigned int year, unsigned int mon,unsigned int day, unsigned int hour,unsigned int min, unsigned int sec)
{if (0 >= (int) (mon -= 2)) { /* 1..12 -> 11,12,1..10 */mon += 12; /* Puts Feb last since it has leap day */year -= 1;}return ((((unsigned long) (year/4 - year/100 + year/400 + 367*mon/12 + day) +year*365 - 719499)*24 + hour /* now have hours */)*60 + min /* now have minutes */)*60 + sec; /* finally seconds */
}
1. 函数定义和参数
static inline unsigned long
mktime (unsigned int year, unsigned int mon,unsigned int day, unsigned int hour,unsigned int min, unsigned int sec)
- 将年月日时分秒转换为从某个纪元开始计算的秒数
- 参数:年、月(1-12)、日、时、分、秒
2. 月份调整部分
if (0 >= (int) (mon -= 2)) { /* 1..12 -> 11,12,1..10 */mon += 12; /* Puts Feb last since it has leap day */year -= 1;}
调整前: 1月=1, 2月=2, …, 12月=12
调整后: 3月=1, 4月=2, …, 2月=12
具体步骤:
mon -= 2 // 月份减2:1月→-1, 2月→0, 3月→1, ..., 12月→10
如果月份≤0(即1月或2月):
mon += 12 // -1→11, 0→12
year -= 1 // 把1月、2月算作前一年的13月、14月
为什么这样调整?
- 将闰年敏感的2月放在一年最后
- 简化闰年计算,因为现在从3月开始到次年2月结束,所以2月的天数直接加就好,不用算是28还是29
3. 核心计算公式
return ((((unsigned long) (year/4 - year/100 + year/400 + 367*mon/12 + day) +year*365 - 719499)*24 + hour /* now have hours */)*60 + min /* now have minutes */)*60 + sec; /* finally seconds */
3.1. 步骤1:计算天数
year/4 - year/100 + year/400 + 367*mon/12 + day + year*365 - 719499
这个公式计算从某个参考日期到目标日期的总天数
年份基础天数
year*365
- 最基本的:每年按365天计算
- 但这样没有考虑闰年,所以需要后面的修正
闰年修正
year/4 - year/100 + year/400
这是计算从公元1年到目标年份之间有多少个闰日的经典公式
year/4
:每4年一个闰年
- 年份1-4:1个闰年 → 1个闰日
- 年份1-100:25个闰年 → 25个闰日
- year/100
:减去世纪年(100的倍数不是闰年)
- 年份1-100:减去1个(100年不是闰年)→ 24个闰日
- 年份1-200:减去2个 → 48个闰日
+ year/400
:加回400的倍数(是闰年)
- 年份1-400:加回1个(400年是闰年)→ 97个闰日
月份天数计算
367*mon/12 + day
367 / 12
约等于30.58
,每年平均月份天数约等于30.43
调整后月份 | 实际月份 | 每月天数 | 累计天数(从3月1日开始) | 367×mon | 367×mon /12 | 差值 |
---|---|---|---|---|---|---|
1 | 3月 | 31 | 0 | 367 | 30 | 30 |
2 | 4月 | 30 | 31 | 734 | 61 | 30 |
3 | 5月 | 31 | 61 | 1101 | 91 | 30 |
4 | 6月 | 30 | 92 | 1468 | 122 | 30 |
5 | 7月 | 31 | 122 | 1835 | 152 | 30 |
6 | 8月 | 31 | 153 | 2202 | 183 | 30 |
7 | 9月 | 30 | 184 | 2569 | 214 | 30 |
8 | 10月 | 31 | 214 | 2936 | 244 | 30 |
9 | 11月 | 30 | 245 | 3303 | 275 | 30 |
10 | 12月 | 31 | 275 | 3670 | 305 | 30 |
11 | 1月 | 31 | 306 | 4037 | 336 | 30 |
12 | 2月 | 28/29 | 337 | 4404 | 367 | 30 |
- 从表格可以看到
367×mon/12
每月的计算结果和实际上每月的累计天数依次大于30,所以367×mon/12
的计算结果减去30就是当月的累积的天数
纪元调整
- 719499
这个魔法数字将计算结果调整到Unix纪元(1970年1月1日)
719499的由来:
计算从公元1年1月1日到1970年1月1日的天数:
步骤1:1969个完整年的天数
1969*365 + (1969/4 - 1969/100 + 1969/400)
= 1969*365 + (492 - 19 + 4)
= 718,685 + 477
= 719,162 天
步骤2:调整到1970年1月1日
由于我们之前的月份计算从3月开始,需要额外调整:
- 719162 + 367*11 / 12 + 1 = 719499 (实际上包含了差值30)
所以 - 719499
就是从公元1年调整到Unix纪元1970年,同时平衡了前面的差值30
3.2. 步骤2:逐级转换为秒数
(天数)*24 + hour // 转换为小时
(小时数)*60 + min // 转换为分钟
(分钟数)*60 + sec // 转换为秒数
七、以timeval
格式返回当前时间do_gettimeofday
void do_gettimeofday(struct timeval *tv)
{unsigned long seq;unsigned long usec, sec;unsigned long max_ntp_tick;do {unsigned long lost;seq = read_seqbegin(&xtime_lock);usec = cur_timer->get_offset();lost = jiffies - wall_jiffies;/** If time_adjust is negative then NTP is slowing the clock* so make sure not to go into next possible interval.* Better to lose some accuracy than have time go backwards..*/if (unlikely(time_adjust < 0)) {max_ntp_tick = (USEC_PER_SEC / HZ) - tickadj;usec = min(usec, max_ntp_tick);if (lost)usec += lost * max_ntp_tick;}else if (unlikely(lost))usec += lost * (USEC_PER_SEC / HZ);sec = xtime.tv_sec;usec += (xtime.tv_nsec / 1000);} while (read_seqretry(&xtime_lock, seq));while (usec >= 1000000) {usec -= 1000000;sec++;}tv->tv_sec = sec;tv->tv_usec = usec;
}
1. 函数定义和变量声明
void do_gettimeofday(struct timeval *tv)
{unsigned long seq;unsigned long usec, sec;unsigned long max_ntp_tick;
- 作用:获取当前时间,填充到
timeval
结构体(秒+微秒) seq
:顺序锁序列号,用于数据一致性保护usec, sec
:存储计算出的秒和微秒max_ntp_tick
:NTP调整时的最大tick值
2. 顺序锁保护的数据读取循环
do {unsigned long lost;seq = read_seqbegin(&xtime_lock);
do-while
循环:使用顺序锁确保读取到一致的时间数据lost
:记录丢失的tick数(中断延迟导致)read_seqbegin()
:开始读取序列,获取当前序列号
3. 时间偏移量计算
usec = cur_timer->get_offset();lost = jiffies - wall_jiffies;
cur_timer->get_offset()
:从硬件定时器获取当前tick内的微秒偏移量lost = jiffies - wall_jiffies
:wall_jiffies
墙上时钟不会每次时钟中断都更新
4. NTP时间调整处理
if (unlikely(time_adjust < 0)) {max_ntp_tick = (USEC_PER_SEC / HZ) - tickadj;usec = min(usec, max_ntp_tick);if (lost)usec += lost * max_ntp_tick;}else if (unlikely(lost))usec += lost * (USEC_PER_SEC / HZ);
4.1. 情况1:NTP调慢时钟(time_adjust < 0)
max_ntp_tick = (USEC_PER_SEC / HZ) - tickadj;
usec = min(usec, max_ntp_tick);
- 当系统时钟比真实时间快时,NTP需要调慢系统时钟,实现方式是让每个tick的时间变短
tickadj
:每次tick的调整量max_ntp_tick
: 计算调整后的最大tick时间min(usec, max_ntp_tick);
: 限制当前tick内的时间不能超过调整后的最大值
if (lost)usec += lost * max_ntp_tick;
- 为丢失的tick补偿时间,但使用调整后的tick长度
- 这样补偿的时间也是"调慢后"的时间
4.2. 情况2:NTP调快或正常(time_adjust ≥ 0)但有丢失tick
else if (unlikely(lost))usec += lost * (USEC_PER_SEC / HZ);
- 使用标准的tick长度进行补偿
5. 基础时间获取
sec = xtime.tv_sec;usec += (xtime.tv_nsec / 1000);
xtime.tv_sec
:从系统核心时间变量获取秒数xtime.tv_nsec / 1000
:将纳秒转换为微秒并累加
6. 一致性验证
} while (read_seqretry(&xtime_lock, seq));
- 检查在读取过程中是否有写操作修改了时间数据
- 如果序列号变化,说明数据可能不一致,重新读取
7. 时间规范化
while (usec >= 1000000) {usec -= 1000000;sec++;}
处理微秒溢出:如果微秒超过1秒,进位到秒
8. 结果输出
tv->tv_sec = sec;tv->tv_usec = usec;
}
- 将计算出的秒和微秒存入输出参数
八、以timespec
格式返回当前时间current_kernel_time
struct timespec current_kernel_time(void)
{struct timespec now;unsigned long seq;do {seq = read_seqbegin(&xtime_lock);now = xtime;} while (read_seqretry(&xtime_lock, seq));return now;
}
1. 函数定义和变量声明
struct timespec current_kernel_time(void)
{struct timespec now;unsigned long seq;
- 返回值:
struct timespec
包含秒和纳秒的时间结构体 - 作用:安全地获取内核维护的当前时间
now
:用于存储读取到的时间值seq
:顺序锁的序列号,用于一致性检查
2. 顺序锁保护的数据读取循环
do {seq = read_seqbegin(&xtime_lock);now = xtime;} while (read_seqretry(&xtime_lock, seq));
这是典型的读-复制-验证模式,确保读取到一致的时间数据
步骤1:开始读取序列
seq = read_seqbegin(&xtime_lock);
read_seqbegin()
:读取顺序锁的当前序列号- 如果序列号是偶数,表示没有写操作在进行
- 如果序列号是奇数,表示有写操作正在进行
步骤2:读取时间数据
now = xtime;
xtime
:内核维护的当前时间变量(struct timespec
类型)- 这是一个结构体拷贝,不是指针引用
步骤3:验证数据一致性
while (read_seqretry(&xtime_lock, seq));
- 检查序列号是否与开始时相同
- 如果不同,说明在我们读取过程中有写操作发生,需要重新读取
3. 返回值
return now;
}
- 返回读取到的时间结构体副本
4. 与其他时间函数的区别
4.1. vs do_gettimeofday()
// current_kernel_time: 返回内核维护的粗略时间
// do_gettimeofday: 返回高精度时间current_kernel_time() → 直接返回xtime
do_gettimeofday() → xtime + NTP调整 + 丢失tick补偿