【Linux内核代码分析1】Linux时间子系统及HRTIMER实现

news2024/11/25 18:26:39

Linux时间子系统软件架构

(1)嵌入式设备需要较好的电源管理策略。传统的linux会有一个周期性的时钟,即便是系统无事可做的时候也要醒来,这样导致系统不断的从低功耗(idle)状态进入高功耗的状态。这样的设计不符合电源管理的需求。
(2)多媒体的应用程序需要非常精确的timer,例如为了避免视频的跳帧、音频回放中的跳动,这些需要系统提供足够精度的timer
和低精度timer不同,高精度timer使用了人类的最直观的时间单位ns(低精度timer使用的tick是和内核配置相关,不够直接)。本质上linux

早期时间子系统如图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dzEuKu8b-1670082567895)(image/Linux内核代码分析1时间子系统/1670002173041.png)]

kernel提供了高精度timer之后,其实不必提供低精度timer了,不过由于低精度timer存在了很长的历史,并且在渗入到内核各个部分,如果去掉低精度timer很容易引起linux
kernel稳定性和健壮性的问题,因此目前的linux kernel保持了低精度timer和高精度timer并存。
在新的需求的推动下,内核开发者对linux的时间子系统的软件框架进行修改,让代码层次更清晰,同时又是灵活可配置的,一个示意性的block图如下所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mqgaU2V9-1670082567896)(image/Linux内核代码分析1时间子系统/1669990265747.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V660t8wR-1670082567896)(image/Linux内核代码分析1时间子系统/1670048778829.png)]

Linux kernel 时间子系统的源文件位于linux/kernel/time/目录下,整理如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jIsbRavq-1670082567896)(image/Linux内核代码分析1时间子系统/1669990385021.png)]

在Linux内核中有两种不同的clock设备,一种是clock source设备,另一种是clock event设备。Clocksource设备一般是一个根据固定频率不停增加的计数器,内核利用该设备可以计算出从系统启动到当前所经过的时间,再加上RTC所提供的初始时间就能得到当前时间(墙上时间)。Clockevent设备则用来提供中断,Clockevent设备可以配置为按固定周期发生中断(periodic)或者产生一个特定时间间隔后的中断(onshot)。

Linux时间描述

jiffies

内核用jiffies变量记录系统启动以来经过的时钟滴答数,它的声明如下:

extern u64 __jiffy_data jiffies_64;
extern unsigned long volatile __jiffy_data jiffies;

在32位的系统上,jiffies是一个32位的无符号数,系统每过1/HZ秒,jiffies的值就会加1,最终该变量可能会溢出,所以内核同时又定义了一个64位的变量jiffies_64,链接的脚本保证jiffies变量和jiffies_64变量的内存地址是相同的,通常,我们可以直接访问jiffies变量,但是要获得jiffies_64变量,必须通过辅助函数get_jiffies_64来实现。jiffies是内核的低精度定时器的计时单位,所以内核配置的HZ数决定了低精度定时器的精度,如果HZ数被设定为1000,那么,低精度定时器(timer_list)的精度就是1ms=1/1000秒。因为jiffies变量可能存在溢出的问题,所以在用基于jiffies进行比较时,应该使用以下辅助宏来实现

time_after(a,b)
time_before(a,b)
time_after_eq(a,b)
time_before_eq(a,b)
time_in_range(a,b,c)

同时,内核还提供了一些辅助函数用于jiffies和毫秒以及纳秒之间的转换

unsigned int jiffies_to_msecs(const unsigned long j);
unsigned int jiffies_to_usecs(const unsigned long j);
unsigned long msecs_to_jiffies(const unsigned int m);
unsigned long usecs_to_jiffies(const unsigned int u);

struct timeval

struct timeval {
	__kernel_time_t		tv_sec;		/* seconds /
	__kernel_suseconds_t	tv_usec;	/ microseconds */
};

__kernel_time_t和 __kernel_suseconds_t 实际上都是long型的整数。gettimeofday和settimeofday使用timeval作为时间单位

struct timespec

struct timespec {
	__kernel_time_t	tv_sec;			/* seconds */
	long		tv_nsec;		/* nanoseconds */
};

timekeeper中的xtime字段用timespec作为时间单位。

struct ktime

union ktime {
	s64	tv64;
#if BITS_PER_LONG != 64 && !defined(CONFIG_KTIME_SCALAR)
	struct {
# ifdef __BIG_ENDIAN
	s32	sec, nsec;
# else
	s32	nsec, sec;
# endif
	} tv;
#endif
};

clocksource 设备

struct clocksource结构

struct clocksource {
	/*
	 * Hotpath data, fits in a single cache line when the
	 * clocksource itself is cacheline aligned.
	 */
	cycle_t (*read)(struct clocksource *cs);
	cycle_t cycle_last;
	cycle_t mask;
	u32 mult;
	u32 shift;
	u64 max_idle_ns;
	u32 maxadj;
#ifdef CONFIG_ARCH_CLOCKSOURCE_DATA
	struct arch_clocksource_data archdata;
#endif
 
	const char *name;
	struct list_head list;
	int rating;
	int (*enable)(struct clocksource *cs);
	void (*disable)(struct clocksource *cs);
	unsigned long flags;
	void (*suspend)(struct clocksource *cs);
	void (*resume)(struct clocksource *cs);
 
	/* private: */
#ifdef CONFIG_CLOCKSOURCE_WATCHDOG
	/* Watchdog related data, used by the framework */
	struct list_head wd_list;
	cycle_t cs_last;
	cycle_t wd_last;
#endif
} ____cacheline_aligned;

rating:时钟源的精度

同一个设备下,可以有多个时钟源,每个时钟源的精度由驱动它的时钟频率决定,比如一个由10MHz时钟驱动的时钟源,他的精度就是100nS。clocksource结构中有一个rating字段,代表着该时钟源的精度范围,它的取值范围如下:

1--99: 不适合于用作实际的时钟源,只用于启动过程或用于测试;
100--199:基本可用,可用作真实的时钟源,但不推荐;
200--299:精度较好,可用作真实的时钟源;
300--399:很好,精确的时钟源;
400--499:理想的时钟源,如有可能就必须选择它作为时钟源;

read回调函数

时钟源本身不会产生中断,要获得时钟源的当前计数,只能通过主动调用它的read回调函数来获得当前的计数值,注意这里只能获得计数值,也就是所谓的cycle,要获得相应的时间,必须要借助clocksource的mult和shift字段进行转换计算。

mult和shift字段

因为从clocksource中读到的值是一个cycle计数值,要转换为时间,我们必须要知道驱动clocksource的时钟频率F,一个简单的计算就可以完成:

t = cycle/F;

可是clocksource并没有保存时钟的频率F,因为使用上面的公式进行计算,需要使用浮点运算,这在内核中是不允许的,因此,内核使用了另外一个变通的办法,根据时钟的频率和期望的精度,事先计算出两个辅助常数mult和shift,然后使用以下公式进行cycle和t的转换:

t = (cycle * mult) >> shift;

只要我们保证:

F = (1 << shift) / mult;

内核内部使用64位进行该转换计算:

static inline s64 clocksource_cyc2ns(cycle_t cycles, u32 mult, u32 shift)
{
        return ((u64) cycles * mult) >> shift;
}

clocksource注册与初始化

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u7tqyvQR-1670082567897)(image/Linux内核代码分析1时间子系统/1669990562936.png)]

由上图可见,最终大部分工作会转由__clocksource_register_scale完成,该函数首先完成对mult和shift值的计算,然后根据mult和shift值,最终通过clocksource_max_deferment获得该clocksource可接受的最大IDLE时间,并记录在clocksource的max_idle_ns字段中。clocksource_enqueue函数负责按clocksource的rating的大小,把该clocksource按顺序挂在全局链表clocksource_list上,rating值越大,在链表上的位置越靠前。
每次新的clocksource注册进来,都会触发clocksource_select函数被调用,它按照rating值选择最好的clocksource,并记录在全局变量curr_clocksource中,然后通过timekeeping_notify函数通知timekeeping,当前clocksource已经变更。

注册clocksource

在系统的启动阶段,内核注册了一个基于jiffies的clocksource,代码位于kernel/time/jiffies.c

struct clocksource clocksource_jiffies = {
	.name		= "jiffies",
	.rating		= 1, /* lowest valid rating*/
	.read		= jiffies_read,
	.mask		= 0xffffffff, /*32bits*/
	.mult		= NSEC_PER_JIFFY << JIFFIES_SHIFT, /* details above */
	.shift		= JIFFIES_SHIFT,
};
......
 
static int __init init_jiffies_clocksource(void)
{
	return clocksource_register(&clocksource_jiffies);
}
 
core_initcall(init_jiffies_clocksource);

它的精度只有1/HZ秒,rating值为1,如果平台的代码没有提供定制的clocksource_default_clock函数,它将返回该clocksource

然后,在初始化的后段,clocksource的代码会把全局变量curr_clocksource设置为上述的clocksource:

static int __init clocksource_done_booting(void)
{
	mutex_lock(&clocksource_mutex);
	curr_clocksource = clocksource_default_clock();
	finished_booting = 1;
	__clocksource_watchdog_kthread();
	clocksource_select();
	mutex_unlock(&clocksource_mutex);
	return 0;
}

如果平台级的代码在初始化时也会注册真正的硬件clocksource,所以经过clocksource_select()函数后,curr_clocksource将会被设为最合适的clocksource。如果clocksource_select函数认为需要切换更好的时钟源,它会通过timekeeping_notify通知timekeeping系统,使用新的clocksource进行时间计数和更新操作。

时间维护者timekeeper

clocksource,以及内核内部时间的一些表示方法,但是对于真实的用户来说,我们感知的是真实世界的真实时间,也就是所谓的墙上时间,clocksource只能提供一个按给定频率不停递增的周期计数,timekeeper 把它和真实的墙上时间相关联

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EUxA7JcH-1670082567897)(image/Linux内核代码分析1时间子系统/1669992182637.png)]

RTC时间 在PC中,RTC时间又叫CMOS时间,它通常由一个专门的计时硬件来实现,软件可以读取该硬件来获得年月日、时分秒等时间信息,而在嵌入式系统中,有使用专门的RTC芯片,也有直接把RTC集成到Soc芯片中,读取Soc中的某个寄存器即可获取当前时间信息。一般来说,RTC是一种可持续计时的,也就是说,不管系统是否上电,RTC中的时间信息都不会丢失,计时会一直持续进行,硬件上通常使用一个后备电池对RTC硬件进行单独的供电。因为RTC硬件的多样性,开发者需要为每种RTC时钟硬件提供相应的驱动程序,内核和用户空间通过驱动程序访问RTC硬件来获取或设置时间信息。

xtime xtime和RTC时间一样,都是人们日常所使用的墙上时间,只是RTC时间的精度通常比较低,大多数情况下只能达到毫秒级别的精度,如果是使用外部的RTC芯片,访问速度也比较慢,为此,内核维护了另外一个wall time时间:xtime,取决于用于对xtime计时的clocksource,它的精度甚至可以达到纳秒级别,因为xtime实际上是一个内存中的变量,它的访问速度非常快,内核大部分时间都是使用xtime来获得当前时间信息。xtime记录的是自1970年1月1日24时到当前时刻所经历的纳秒数。

monotonic time 该时间自系统开机后就一直单调地增加,它不像xtime可以因用户的调整时间而产生跳变,不过该时间不计算系统休眠的时间,也就是说,系统休眠时,monotoic时间不会递增。

raw monotonic time 该时间与monotonic时间类似,也是单调递增的时间,唯一的不同是:raw monotonic time“更纯净”,他不会受到NTP时间调整的影响,它代表着系统独立时钟硬件对时间的统计。

boot time 与monotonic时间相同,不过会累加上系统休眠的时间,它代表着系统上电后的总时间。

struct timekeeper

struct timekeeper {
	struct clocksource *clock;    /* Current clocksource used for timekeeping. */
	u32	mult;    /* NTP adjusted clock multiplier */
	int	shift;	/* The shift value of the current clocksource. */
	cycle_t cycle_interval;	/* Number of clock cycles in one NTP interval. */
	u64	xtime_interval;	/* Number of clock shifted nano seconds in one NTP interval. */
	s64	xtime_remainder;	/* shifted nano seconds left over when rounding cycle_interval */
	u32	raw_interval;	/* Raw nano seconds accumulated per NTP interval. */
 
	u64	xtime_nsec;	/* Clock shifted nano seconds remainder not stored in xtime.tv_nsec. */
	/* Difference between accumulated time and NTP time in ntp
	 * shifted nano seconds. */
	s64	ntp_error;
	/* Shift conversion between clock shifted nano seconds and
	 * ntp shifted nano seconds. */
	int	ntp_error_shift;
 
	struct timespec xtime;	/* The current time */
 
	struct timespec wall_to_monotonic;
	struct timespec total_sleep_time;	/* time spent in suspend */
	struct timespec raw_time;	/* The raw monotonic time for the CLOCK_MONOTONIC_RAW posix clock. */
 
	ktime_t offs_real;	/* Offset clock monotonic -> clock realtime */
 
	ktime_t offs_boot;	/* Offset clock monotonic -> clock boottime */
 
	seqlock_t lock;	/* Seqlock for all timekeeper values */
};

初始化

timekeeper的初始化由timekeeping_init完成,该函数在start_kernel的初始化序列中被调用,timekeeping_init首先从RTC中获取当前时间:

void __init timekeeping_init(void)
{
	struct clocksource *clock;
	unsigned long flags;
	struct timespec now, boot;
 
	read_persistent_clock(&now);
	read_boot_clock(&boot);
	seqlock_init(&timekeeper.lock);
	ntp_init();

接着获取默认的clocksource,如果平台没有重新实现clocksource_default_clock函数,默认的clocksource就是基于jiffies的clocksource_jiffies,然后通过timekeeper_setup_inernals内部函数把timekeeper和clocksource进行关联:

	write_seqlock_irqsave(&timekeeper.lock, flags);
	clock = clocksource_default_clock();
	if (clock->enable)
		clock->enable(clock);
	timekeeper_setup_internals(clock);

利用RTC的当前时间,初始化xtime,raw_time,wall_to_monotonic等字段:

	timekeeper.xtime.tv_sec = now.tv_sec;
	timekeeper.xtime.tv_nsec = now.tv_nsec;
	timekeeper.raw_time.tv_sec = 0;
	timekeeper.raw_time.tv_nsec = 0;
	if (boot.tv_sec == 0 && boot.tv_nsec == 0) {
		boot.tv_sec = timekeeper.xtime.tv_sec;
		boot.tv_nsec = timekeeper.xtime.tv_nsec;
	}
	set_normalized_timespec(&timekeeper.wall_to_monotonic,
				-boot.tv_sec, -boot.tv_nsec);

最后,初始化代表实时时间和monotonic时间之间偏移量的offs_real字段,total_sleep_time字段初始化为0:

	update_rt_offset();
	timekeeper.total_sleep_time.tv_sec = 0;
	timekeeper.total_sleep_time.tv_nsec = 0;
	write_sequnlock_irqrestore(&timekeeper.lock, flags);
}

xtime字段因为是保存在内存中,系统掉电后无法保存时间信息,所以每次启动时都要通过timekeeping_init从RTC中同步正确的时间信息。其中,read_persistent_clock和read_boot_clock是平台级的函数,分别用于获取RTC硬件时间和启动时的时间,不过值得注意到是。如果平台没有实现该函数,内核提供了一个默认的实现:

void __attribute__((weak)) read_persistent_clock(struct timespec *ts)
{
	ts->tv_sec = 0;
	ts->tv_nsec = 0;
}

时间更新

xtime一旦初始化完成后,timekeeper就开始独立于RTC,利用自身关联的clocksource进行时间的更新操作,根据内核的配置项的不同,更新时间的操作发生的频度也不尽相同,如果没有配置NO_HZ选项,通常每个tick的定时中断周期,do_timer会被调用一次,相反,如果配置了NO_HZ选项,可能会在好几个tick后,do_timer才会被调用一次,当然传入的参数是本次更新离上一次更新时相隔了多少个tick周期,系统会保证在clocksource的max_idle_ns时间内调用do_timer,以防止clocksource的溢出:

void do_timer(unsigned long ticks)
{
	jiffies_64 += ticks;
	update_wall_time();
	calc_global_load(ticks);
}

获取时间

timekeeper提供了一系列的接口用于获取各种时间信息。

void getboottime(struct timespec *ts);    获取系统启动时刻的实时时间
void get_monotonic_boottime(struct timespec *ts);     获取系统启动以来所经过的时间,包含休眠时间
ktime_t ktime_get_boottime(void);   获取系统启动以来所经过的c时间,包含休眠时间,返回ktime类型
ktime_t ktime_get(void);    获取系统启动以来所经过的c时间,不包含休眠时间,返回ktime类型
void ktime_get_ts(struct timespec *ts) ;   获取系统启动以来所经过的c时间,不包含休眠时间,返回timespec结构
unsigned long get_seconds(void);    返回xtime中的秒计数值
struct timespec current_kernel_time(void);    返回内核最后一次更新的xtime时间,不累计最后一次更新至今clocksource的计数值
void getnstimeofday(struct timespec *ts);    获取当前时间,返回timespec结构
void do_gettimeofday(struct timeval *tv);    获取当前时间,返回timeval结构

clock_event_device 定时设备

早期的内核版本中,进程的调度基于一个称之为tick的时钟滴答,通常使用时钟中断来定时地产生tick信号,每次tick定时中断都会进行进程的统计和调度,并对tick进行计数,记录在一个jiffies变量中,定时器的设计也是基于jiffies。这时候的内核代码中,几乎所有关于时钟的操作都是在machine级的代码中实现,很多公共的代码要在每个平台上重复实现。随后,随着通用时钟框架的引入,内核需要支持高精度的定时器,为此,通用时间框架为定时器硬件定义了一个标准的接口:clock_event_device,machine级的代码只要按这个标准接口实现相应的硬件控制功能,剩下的与平台无关的特性则统一由通用时间框架层来实现。

clocksource不能被编程,没有产生事件的能力,它主要被用于timekeeper来实现对真实时间进行精确的统计,而clock_event_device则是可编程的,它可以工作在周期触发或单次触发模式,系统可以对它进行编程,以确定下一次事件触发的时间,clock_event_device主要用于实现普通定时器和高精度定时器,同时也用于产生tick事件,供给进程调度子系统使用。时钟事件设备与通用时间框架中的其他模块的关系如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dV37XPUX-1670082567897)(image/Linux内核代码分析1时间子系统/1669993973445.png)]

与clocksource一样,系统中可以存在多个clock_event_device,系统会根据它们的精度和能力,选择合适的clock_event_device对系统提供时钟事件服务。在smp系统中,为了减少处理器间的通信开销,基本上每个cpu都会具备一个属于自己的本地clock_event_device,独立地为该cpu提供时钟事件服务,smp中的每个cpu基于本地的clock_event_device,建立自己的tick_device,普通定时器和高精度定时器。
在软件架构上看,clock_event_device被分为了两层,与硬件相关的被放在了machine层,而与硬件无关的通用代码则被集中到了通用时间框架层,这符合内核对软件的设计需求,平台的开发者只需实现平台相关的接口即可,无需关注复杂的上层时间框架。
tick_device是基于clock_event_device的进一步封装,用于代替原有的时钟滴答中断,给内核提供tick事件,以完成进程的调度和进程信息统计,负载平衡和时间更新等操作。

时钟事件设备相关数据结构

struct clock_event_device

时钟事件设备的核心数据结构是clock_event_device结构,它代表着一个时钟硬件设备,该设备就好像是一个具有事件触发能力(通常就是指中断)的clocksource,它不停地计数,当计数值达到预先编程设定的数值那一刻,会引发一个时钟事件中断,继而触发该设备的事件处理回调函数,以完成对时钟事件的处理。clock_event_device结构的定义如下:

struct clock_event_device {
	void			(*event_handler)(struct clock_event_device *);
	int			(*set_next_event)(unsigned long evt,
						  struct clock_event_device *);
	int			(*set_next_ktime)(ktime_t expires,
						  struct clock_event_device *);
	ktime_t			next_event;
	u64			max_delta_ns;
	u64			min_delta_ns;
	u32			mult;
	u32			shift;
	enum clock_event_mode	mode;
	unsigned int		features;
	unsigned long		retries;
 
	void			(*broadcast)(const struct cpumask *mask);
	void			(*set_mode)(enum clock_event_mode mode,
					    struct clock_event_device *);
	unsigned long		min_delta_ticks;
	unsigned long		max_delta_ticks;
 
	const char		*name;
	int			rating;
	int			irq;
	const struct cpumask	*cpumask;
	struct list_head	list;
} ____cacheline_aligned;

event_handler 该字段是一个回调函数指针,通常由通用框架层设置,在时间中断到来时,machine底层的的中断服务程序会调用该回调,框架层利用该回调实现对时钟事件的处理。

set_next_event 设置下一次时间触发的时间,使用类似于clocksource的cycle计数值(离现在的cycle差值)作为参数。

set_next_ktime 设置下一次时间触发的时间,直接使用ktime时间作为参数。

max_delta_ns 可设置的最大时间差,单位是纳秒。

min_delta_ns 可设置的最小时间差,单位是纳秒。

mult shift 与clocksource中的类似,只不过是用于把纳秒转换为cycle。

mode 该时钟事件设备的工作模式,两种主要的工作模式分别是:

CLOCK_EVT_MODE_PERIODIC  周期触发模式,设置后按给定的周期不停地触发事件;
CLOCK_EVT_MODE_ONESHOT  单次触发模式,只在设置好的触发时刻触发一次;

set_mode 函数指针,用于设置时钟事件设备的工作模式。

rating 表示该设备的精度等级。

list 系统中注册的时钟事件设备用该字段挂在全局链表变量clockevent_devices上。

全局变量clockevent_devices

系统中所有注册的clock_event_device都会挂在该链表下面,它在kernel/time/clockevents.c中定义:

static LIST_HEAD(clockevent_devices);

全局变量clockevents_chain

通用时间框架初始化时会注册一个通知链(NOTIFIER),当系统中的时钟时间设备的状态发生变化时,利用该通知链通知系统的其它模块。

/* Notification for clock events */
static RAW_NOTIFIER_HEAD(clockevents_chain);

初始化

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b9IlHVEi-1670082567898)(image/Linux内核代码分析1时间子系统/1669994414729.png)]

start_kernel调用了time_init函数,该函数通常定义在体系相关的代码中,正如前面所讨论的一样,它主要完成machine级别对时钟系统的初始化工作,最终通过clockevents_register_device注册系统中的时钟事件设备,把每个时钟时间设备挂在clockevent_device全局链表上,最后通过clockevent_do_notify触发框架层事先注册好的通知链,其实就是调用了tick_notify函数,我们主要关注CLOCK_EVT_NOTIFY_ADD通知,其它通知请自行参考代码,下面是tick_notify的简化版本:

static int tick_notify(struct notifier_block *nb, unsigned long reason,
			       void *dev)
{
	switch (reason) {
 
	case CLOCK_EVT_NOTIFY_ADD:
		return tick_check_new_device(dev);
 
	case CLOCK_EVT_NOTIFY_BROADCAST_ON:
	case CLOCK_EVT_NOTIFY_BROADCAST_OFF:
	case CLOCK_EVT_NOTIFY_BROADCAST_FORCE:
            ......
	case CLOCK_EVT_NOTIFY_BROADCAST_ENTER:
	case CLOCK_EVT_NOTIFY_BROADCAST_EXIT:
            ......
	case CLOCK_EVT_NOTIFY_CPU_DYING:
            ......
	case CLOCK_EVT_NOTIFY_CPU_DEAD:
            ......
	case CLOCK_EVT_NOTIFY_SUSPEND:
            ......
	case CLOCK_EVT_NOTIFY_RESUME:
            ......
	}
 
	return NOTIFY_OK;
}

可见,对于新注册的clock_event_device,会发出CLOCK_EVT_NOTIFY_ADD通知,最终会进入函数:tick_check_new_device,这个函数比对当前cpu所使用的与新注册的clock_event_device之间的特性,如果认为新的clock_event_device更好,则会进行切换工作。下一节将会详细的讨论该函数。到这里,每个cpu已经有了自己的clock_event_device,在这以后,框架层的代码会根据内核的配置项(CONFIG_NO_HZ、CONFIG_HIGH_RES_TIMERS),对注册的clock_event_device进行不同的设置,从而为系统的tick和高精度定时器提供服务,这些内容我们留在本系列的后续文章进行讨论。

tick_device

当内核没有配置成支持高精度定时器时,系统的tick由tick_device产生,tick_device其实是clock_event_device的简单封装,它内嵌了一个clock_event_device指针和它的工作模式:

struct tick_device {
	struct clock_event_device *evtdev;
	enum tick_device_mode mode;
};

在kernel/time/tick-common.c中,定义了一个per-cpu的tick_device全局变量,tick_cpu_device:

DEFINE_PER_CPU(struct tick_device, tick_cpu_device);

前面曾经说过,当machine的代码为每个cpu注册clock_event_device时,通知回调函数tick_notify会被调用,进而进入tick_check_new_device函数,下面让我们看看该函数如何工作,首先,该函数先判断注册的clock_event_device是否可用于本cpu,然后从per-cpu变量中取出本cpu的tick_device:

void tick_check_new_device(struct clock_event_device *newdev)
{
	struct clock_event_device *curdev;
	struct tick_device *td;
	int cpu;

	cpu = smp_processor_id();
	if (!cpumask_test_cpu(cpu, newdev->cpumask))
		goto out_bc;

	td = &per_cpu(tick_cpu_device, cpu);
	curdev = td->evtdev;

	/* cpu local device ? */
	if (!tick_check_percpu(curdev, newdev, cpu))
		goto out_bc;

	/* Preference decision */
	if (!tick_check_preferred(curdev, newdev))
		goto out_bc;

	if (!try_module_get(newdev->owner))
		return;

	/*
	 * Replace the eventually existing device by the new
	 * device. If the current device is the broadcast device, do
	 * not give it back to the clockevents layer !
	 */
	if (tick_is_broadcast_device(curdev)) {
		clockevents_shutdown(curdev);
		curdev = NULL;
	}
	clockevents_exchange_device(curdev, newdev);
	tick_setup_device(td, newdev, cpu, cpumask_of(cpu));
	if (newdev->features & CLOCK_EVT_FEAT_ONESHOT)
		tick_oneshot_notify();
	return;

out_bc:
	/*
	 * Can the new device be used as a broadcast device ?
	 */
	tick_install_broadcast_device(newdev);
}

static void tick_setup_device(struct tick_device *td,
			      struct clock_event_device *newdev, int cpu,
			      const struct cpumask *cpumask)
{
	ktime_t next_event;
	void (*handler)(struct clock_event_device *) = NULL;

	/*
	 * First device setup ?
	 */
	if (!td->evtdev) {
		/*
		 * If no cpu took the do_timer update, assign it to
		 * this cpu:
		 */
		if (tick_do_timer_cpu == TICK_DO_TIMER_BOOT) {
			if (!tick_nohz_full_cpu(cpu))
				tick_do_timer_cpu = cpu;
			else
				tick_do_timer_cpu = TICK_DO_TIMER_NONE;
			tick_next_period = ktime_get();
			tick_period = ktime_set(0, NSEC_PER_SEC / HZ);
		}

		/*
		 * Startup in periodic mode first.
		 */
		td->mode = TICKDEV_MODE_PERIODIC;
	} else {
		handler = td->evtdev->event_handler;
		next_event = td->evtdev->next_event;
		td->evtdev->event_handler = clockevents_handle_noop;
	}

	td->evtdev = newdev;

	/*
	 * When the device is not per cpu, pin the interrupt to the
	 * current cpu:
	 */
	if (!cpumask_equal(newdev->cpumask, cpumask))
		irq_set_affinity(newdev->irq, cpumask);

	/*
	 * When global broadcasting is active, check if the current
	 * device is registered as a placeholder for broadcast mode.
	 * This allows us to handle this x86 misfeature in a generic
	 * way. This function also returns !=0 when we keep the
	 * current active broadcast state for this CPU.
	 */
	if (tick_device_uses_broadcast(newdev, cpu))
		return;

	if (td->mode == TICKDEV_MODE_PERIODIC)
		tick_setup_periodic(newdev, 0);
	else
		tick_setup_oneshot(newdev, handler, next_event);
}

在系统的启动阶段,tick_device是工作在周期触发模式的,直到框架层在合适的时机,才会开启单触发模式,以便支持NO_HZ和HRTIMER。

第一个注册的tick_device必然工作在TICKDEV_MODE_PERIODIC模式,所以tick_setup_periodic会设置clock_event_device的事件回调字段event_handler为tick_handle_periodic,工作一段时间后,就算有新的支持TICKDEV_MODE_ONESHOT模式的clock_event_device需要替换,再次进入tick_setup_device函数,tick_setup_oneshot的handler参数也是之前设置的tick_handle_periodic函数,所以我们考察tick_handle_periodic即可:

void tick_handle_periodic(struct clock_event_device *dev)
{
	int cpu = smp_processor_id();
	ktime_t next;
 
	tick_periodic(cpu);
 
	if (dev->mode != CLOCK_EVT_MODE_ONESHOT)
		return;
 
	next = ktime_add(dev->next_event, tick_period);
	for (;;) {
		if (!clockevents_program_event(dev, next, false))
			return;
		if (timekeeping_valid_for_hres())
			tick_periodic(cpu);
		next = ktime_add(next, tick_period);
	}
}

该函数首先调用tick_periodic函数,完成tick事件的所有处理,如果是周期触发模式,处理结束,如果工作在单触发模式,则计算并设置下一次的触发时刻,这里用了一个循环,是为了防止当该函数被调用时,clock_event_device中的计时实际上已经经过了不止一个tick周期,这时候,tick_periodic可能被多次调用,使得jiffies和时间可以被正确地更新。tick_periodic的代码如下

static void tick_periodic(int cpu)
{
	if (tick_do_timer_cpu == cpu) {
		write_seqlock(&xtime_lock);
 
		/* Keep track of the next tick event */
		tick_next_period = ktime_add(tick_next_period, tick_period);
 
		do_timer(1);
		write_sequnlock(&xtime_lock);
	}
 
	update_process_times(user_mode(get_irq_regs()));
	profile_tick(CPU_PROFILING);
}

如果当前cpu负责更新时间,则通过do_timer进行以下操作:

更新jiffies_64变量;
更新墙上时钟;
每10个tick,更新一次cpu的负载信息;

调用update_peocess_times,完成以下事情:

更新进程的时间统计信息;
触发TIMER_SOFTIRQ软件中断,以便系统处理传统的低分辨率定时器;
检查rcu的callback;
通过scheduler_tick触发调度系统进行进程统计和调度工作;

低精度定时器时间模型

使用方法

在讨论定时器的实现原理之前,我们先看看如何使用定时器。要在内核编程中使用定时器,首先我们要定义一个time_list结构,该结构在include/linux/timer.h中定义:

struct timer_list {
    	/*
    	 * All fields that change during normal runtime grouped to the
    	 * same cacheline
    	 */
    	struct list_head entry;
    	unsigned long expires;
    	struct tvec_base *base;    	void (*function)(unsigned long);
    	unsigned long data;    	int slack;
            ......
    };

entry 字段用于把一组定时器组成一个链表,至于内核如何对定时器进行分组,我们会在后面进行解释。

expires 字段指出了该定时器的到期时刻,也就是期望定时器到期时刻的jiffies计数值。

base 每个cpu拥有一个自己的用于管理定时器的tvec_base结构,该字段指向该定时器所属的cpu所对应tvec_base结构。

function 字段是一个函数指针,定时器到期时,系统将会调用该回调函数,用于响应该定时器的到期事件。

data 该字段用于上述回调函数的参数。

slack 对有些对到期时间精度不太敏感的定时器,到期时刻允许适当地延迟一小段时间,该字段用于计算每次延迟的HZ数。

要定义一个timer_list,我们可以使用静态和动态两种办法,静态方法使用DEFINE_TIMER宏:

#define DEFINE_TIMER(_name, _function, _expires, _data)

该宏将得到一个名字为_name,并分别用_function,_expires,_data参数填充timer_list的相关字段。

如果要使用动态的方法,则可以自己声明一个timer_list结构,然后手动初始化它的各个字段:

    struct timer_list timer;
    ......
    init_timer(&timer);
    timer.function = _function;
    timer.expires = _expires;
    timer.data = _data;

要激活一个定时器,我们只要调用add_timer即可:

add_timer(&timer);

要修改定时器的到期时间,我们只要调用mod_timer即可:

mod_timer(&timer, jiffies+50);

要移除一个定时器,我们只要调用del_timer即可:

del_timer(&timer);

定时器系统还提供了以下这些API供我们使用:

    void add_timer_on(struct timer_list *timer, int cpu);  // 在指定的cpu上添加定时器
    int mod_timer_pending(struct timer_list *timer, unsigned long expires);  //  只有当timer已经处在激活状态时,才修改timer的到期时刻
    int mod_timer_pinned(struct timer_list *timer, unsigned long expires);  //  当
    void set_timer_slack(struct timer_list *time, int slack_hz);  //  设定timer允许的到期时刻的最大延迟,用于对精度不敏感的定时器
    int del_timer_sync(struct timer_list *timer);  //  如果该timer正在被处理中,则等待timer处理完成才移除该timer

软件架构

低分辨率定时器是基于HZ来实现的,也就是说,每个tick周期,都有可能有定时器到期,关于tick如何产生。系统中有可能有成百上千个定时器,难道在每个tick中断中遍历一下所有的定时器,检查它们是否到期?内核当然不会使用这么笨的办法,它使用了一个更聪明的办法:按定时器的到期时间对定时器进行分组。因为目前的多核处理器使用越来越广泛,连智能手机的处理器动不动就是4核心,内核对多核处理器有较好的支持,低分辨率定时器在实现时也充分地考虑了多核处理器的支持和优化。为了较好地利用cache line,也为了避免cpu之间的互锁,内核为多核处理器中的每个cpu单独分配了管理定时器的相关数据结构和资源,每个cpu独立地管理属于自己的定时器。

首先,内核为每个cpu定义了一个tvec_base结构指针:

static DEFINE_PER_CPU(struct tvec_base *, tvec_bases) = &boot_tvec_bases;

tvec_base结构的定义如下

struct tvec_base {
	spinlock_t lock;
	struct timer_list *running_timer;
	unsigned long timer_jiffies;
	unsigned long next_timer;
	struct tvec_root tv1;
	struct tvec tv2;
	struct tvec tv3;
	struct tvec tv4;
	struct tvec tv5;
} ____cacheline_aligned;

running_timer 该字段指向当前cpu正在处理的定时器所对应的timer_list结构。

timer_jiffies 该字段表示当前cpu定时器所经历过的jiffies数,大多数情况下,该值和jiffies计数值相等,当cpu的idle状态连续持续了多个jiffies时间时,当退出idle状态时,jiffies计数值就会大于该字段,在接下来的tick中断后,定时器系统会让该字段的值追赶上jiffies值。

next_timer 该字段指向该cpu下一个即将到期的定时器。

tv1–tv5 这5个字段用于对定时器进行分组,实际上,tv1–tv5都是一个链表数组,其中tv1的数组大小为TVR_SIZE, tv2 tv3 tv4 tv5的数组大小为TVN_SIZE,根据CONFIG_BASE_SMALL配置项的不同,它们有不同的大小:

当有一个新的定时器要加入时,系统根据定时器到期的jiffies值和timer_jiffies字段的差值来决定该定时器被放入tv1至tv5中的哪一个数组中,最终,系统中所有的定时器的组织结构如下图所示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DI5vT5s9-1670082567898)(image/Linux内核代码分析1时间子系统/1669996289975.png)]

确定链表数组后,接着要确定把该定时器放入数组中的哪一个链表中,如果时间差idx小于256,按规则要放入tv1中,因为tv1包含了256个链表,所以可以简单地使用timer_list.expires的低8位作为数组的索引下标,把定时器链接到tv1中相应的链表中即可。如果时间差idx的值在256–18383之间,则需要把定时器放入tv2中,同样的,使用timer_list.expires的8–14位作为数组的索引下标,把定时器链接到tv2中相应的链表中,。定时器要加入tv3 tv4 tv5使用同样的原理。经过这样分组后的定时器,在后续的tick事件中,系统可以很方便地定位并取出相应的到期定时器进行处理。以上的讨论都体现在internal_add_timer

每个tick事件到来时,内核会在tick定时中断处理期间激活定时器软中断:TIMER_SOFTIRQ的执行函数是__run_timers,它实现了本节讨论的逻辑,取出tv1中到期的定时器,执行定时器的回调函数,由此可见, 低分辨率定时器的回调函数是执行在软件中断上下文中的,这点在写定时器的回调函数时需要注意。__run_timers的代码如下:

static inline void __run_timers(struct tvec_base *base)
{
	struct timer_list *timer;
 
	spin_lock_irq(&base->lock);
        /* 同步jiffies,在NO_HZ情况下,base->timer_jiffies可能落后不止一个tick  */
	while (time_after_eq(jiffies, base->timer_jiffies)) {  
		struct list_head work_list;
		struct list_head *head = &work_list;
                /*  计算到期定时器链表在tv1中的索引  */
		int index = base->timer_jiffies & TVR_MASK;  
 
		/*
		 * /*  tv2--tv5定时器列表迁移处理  */
		 */
		if (!index &&
			(!cascade(base, &base->tv2, INDEX(0))) &&  
				(!cascade(base, &base->tv3, INDEX(1))) &&  
					!cascade(base, &base->tv4, INDEX(2)))  
			cascade(base, &base->tv5, INDEX(3));  
                /*  该cpu定时器系统运行时间递增一个tick  */   
		++base->timer_jiffies;  
                /*  取出到期的定时器链表  */                       
		list_replace_init(base->tv1.vec + index, &work_list);
                /*  遍历所有的到期定时器  */  
		while (!list_empty(head)) {                    
			void (*fn)(unsigned long);
			unsigned long data;
 
			timer = list_first_entry(head, struct timer_list,entry);
			fn = timer->function;
			data = timer->data;
 
			timer_stats_account_timer(timer);
 
			base->running_timer = timer;    /*  标记正在处理的定时器  */
			detach_timer(timer, 1);
 
			spin_unlock_irq(&base->lock);
			call_timer_fn(timer, fn, data);  /*  调用定时器的回调函数  */
			spin_lock_irq(&base->lock);
		}
	}
	base->running_timer = NULL;
	spin_unlock_irq(&base->lock);
}

通过上面的讨论,我们可以发现,内核的低分辨率定时器的实现非常精妙,既实现了大量定时器的管理,又实现了快速的O(1)查找到期定时器的能力,利用巧妙的数组结构,使得只需在间隔256个tick时间才处理一次迁移操作,5个数组就好比是5个齿轮,它们随着base->timer_jifffies的增长而不停地转动,每次只需处理第一个齿轮的某一个齿节,低一级的齿轮转动一圈,高一级的齿轮转动一个齿,同时自动把即将到期的定时器迁移到上一个齿轮中,所以低分辨率定时器通常又被叫做时间轮:time wheel

高精度定时器时间模型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GUELdzlc-1670082567898)(image/Linux内核代码分析1时间子系统/1669999226619.png)]

hrtimer的实现需要一定的硬件基础,它的实现依赖于我们前几章介绍的timekeeper和clock_event_device

hrtimer系统需要通过timekeeper获取当前的时间,计算与到期时间的差值,并根据该差值,设定该cpu的tick_device(clock_event_device)的下一次的到期时间,时间一到,在clock_event_device的事件回调函数中处理到期的hrtimer。现在你或许有疑问:前面在介绍clock_event_device时,我们知道,每个cpu有自己的tick_device,通常用于周期性地产生进程调度和时间统计的tick事件,这里又说要用tick_device调度hrtimer系统,通常cpu只有一个tick_device,那他们如何协调工作?这个问题也一度困扰着我,如果再加上NO_HZ配置带来tickless特性,现在我们只要先知道,一旦开启了hrtimer,tick_device所关联的clock_event_device的事件回调函数会被修改为:hrtimer_interrupt,并且会被设置成工作于CLOCK_EVT_MODE_ONESHOT单触发模式。

低精度模式

因为系统并不是一开始就会支持高精度模式,而是在系统启动后的某个阶段,等待所有的条件都满足后,才会切换到高精度模式,当系统还没有切换到高精度模式时,所有的高精度定时器运行在低精度模式下,在每个jiffie的tick事件中断中进行到期定时器的查询和处理,显然这时候的精度和低分辨率定时器是一样的(HZ级别)。低精度模式下,每个tick事件中断中,hrtimer_run_queues函数会被调用,由它完成定时器的到期处理。hrtimer_run_queues首先判断目前高精度模式是否已经启用,如果已经切换到了高精度模式,什么也不做,直接返回

void hrtimer_run_queues(void)
{
 
	if (hrtimer_hres_active())
		return;

如果hrtimer_hres_active返回false,说明目前处于低精度模式下,则继续处理,它用一个for循环遍历各个时间基准系统,查询每个hrtimer_clock_base对应红黑树的左下节点,判断它的时间是否到期,如果到期,通过__run_hrtimer函数,对到期定时器进行处理,包括:调用定时器的回调函数、从红黑树中移除该定时器、根据回调函数的返回值决定是否重新启动该定时器等等:

高精度模式

hrtimer使用

要添加一个hrtimer,系统提供了一些api供我们使用,首先我们需要定义一个hrtimer结构的实例,然后用hrtimer_init函数对它进行初始化,它的原型如下:

void hrtimer_init(struct hrtimer *timer, clockid_t which_clock, enum hrtimer_mode mode);

which_clock可以是CLOCK_REALTIME、CLOCK_MONOTONIC、CLOCK_BOOTTIME中的一种,mode则可以是相对时间HRTIMER_MODE_REL,也可以是绝对时间HRTIMER_MODE_ABS。设定回调函数:

timer.function = hr_callback;

如果定时器无需指定一个到期范围,可以在设定回调函数后直接使用hrtimer_start激活该定时器:

int hrtimer_start(struct hrtimer *timer, ktime_t tim,
			 const enum hrtimer_mode mode);

如果需要指定到期范围,则可以使用hrtimer_start_range_ns激活定时器:

hrtimer_start_range_ns(struct hrtimer *timer, ktime_t tim, unsigned long range_ns, const enum hrtimer_mode mode);

要取消一个hrtimer,使用hrtimer_cancel:

int hrtimer_cancel(struct hrtimer *timer);

以下两个函数用于推后定时器的到期时间:

extern u64
    hrtimer_forward(struct hrtimer *timer, ktime_t now, ktime_t interval);

    /* Forward a hrtimer so it expires after the hrtimer's current now */
    static inline u64 hrtimer_forward_now(struct hrtimer *timer,
    				      ktime_t interval)
    {
    	return hrtimer_forward(timer, timer->base->get_time(), interval);
    }

软件架构

内核的开发者考察了多种数据结构,例如基数树、哈希表等等,最终他们选择了红黑树(rbtree)来组织hrtimer,红黑树已经以库的形式存在于内核中,并被成功地使用在内存管理子系统和文件系统中,随着系统的运行,hrtimer不停地被创建和销毁,新的hrtimer按顺序被插入到红黑树中,树的最左边的节点就是最快到期的定时器,内核用一个hrtimer结构来表示一个高精度定时器:

struct hrtimer {
	struct timerqueue_node		node;
	ktime_t				_softexpires;
	enum hrtimer_restart		(*function)(struct hrtimer *);
	struct hrtimer_clock_base	*base;
	unsigned long			state;
        ......
};
以下几个函数用于获取定时器的当前状态:

    static inline int hrtimer_active(const struct hrtimer *timer)
    {
    	return timer->state != HRTIMER_STATE_INACTIVE;
    }

    static inline int hrtimer_is_queued(struct hrtimer *timer)
    {
    	return timer->state & HRTIMER_STATE_ENQUEUED;
    }

    static inline int hrtimer_callback_running(struct hrtimer *timer)
    {
    	return timer->state & HRTIMER_STATE_CALLBACK;
    }

定时器的到期时间用ktime_t来表示,_softexpires字段记录了时间,定时器一旦到期,function字段指定的回调函数会被调用,该函数的返回值为一个枚举值,它决定了该hrtimer是否需要被重新激活:

    enum hrtimer_restart {
    	HRTIMER_NORESTART,	/* Timer is not restarted /
    	HRTIMER_RESTART,	/ Timer must be restarted */
    };

state字段用于表示hrtimer当前的状态,有几下几种位组合:

    #define HRTIMER_STATE_INACTIVE	0x00  // 定时器未激活
    #define HRTIMER_STATE_ENQUEUED	0x01  // 定时器已经被排入红黑树中
    #define HRTIMER_STATE_CALLBACK	0x02  // 定时器的回调函数正在被调用
    #define HRTIMER_STATE_MIGRATE	0x04  // 定时器正在CPU之间做迁移

hrtimer的到期时间可以基于以下几种时间基准系统:

    enum  hrtimer_base_type {
    	HRTIMER_BASE_MONOTONIC,  // 单调递增的monotonic时间,不包含休眠时间
    	HRTIMER_BASE_REALTIME,   // 平常使用的墙上真实时间
    	HRTIMER_BASE_BOOTTIME,   // 单调递增的boottime,包含休眠时间
    	HRTIMER_MAX_CLOCK_BASES, // 用于后续数组的定义
    };

和低分辨率定时器一样,处于效率和上锁的考虑,每个cpu单独管理属于自己的hrtimer,为此,专门定义了一个结构hrtimer_cpu_base:

    struct hrtimer_cpu_base {
            ......
    	struct hrtimer_clock_base	clock_base[HRTIMER_MAX_CLOCK_BASES];
    };

其中,clock_base数组为每时间基准系统都定义了一个hrtimer_clock_base结构,它的定义如下:

    struct hrtimer_clock_base {
    	struct hrtimer_cpu_base	*cpu_base;  // 指向所属cpu的hrtimer_cpu_base结构
            ......
    	struct timerqueue_head	active;     // 红黑树,包含了所有使用该时间基准系统的hrtimer
    	ktime_t			resolution; // 时间基准系统的分辨率
    	ktime_t			(*get_time)(void); // 获取该基准系统的时间函数
    	ktime_t			softirq_time;// 当用jiffies
    	ktime_t			offset;      //
    };

active字段是一个timerqueue_head结构,它实际上是对rbtree的进一步封装:

    struct timerqueue_node {
    	struct rb_node node;  // 红黑树的节点
    	ktime_t expires;      // 该节点代表队hrtimer的到期时间,与hrtimer结构中的_softexpires稍有不同
    };
struct timerqueue_head {
    	struct rb_root head;          // 红黑树的根节点
    	struct timerqueue_node *next; // 该红黑树中最早到期的节点,也就是最左下的节点
    };

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PXPyT5ou-1670082567899)(image/Linux内核代码分析1时间子系统/1669996985397.png)]

每个cpu有一个hrtimer_cpu_base结构;
hrtimer_cpu_base结构管理着3种不同的时间基准系统的hrtimer,分别是:实时时间,启动时间和单调时间;
每种时间基准系统通过它的active字段(timerqueue_head结构指针),指向它们各自的红黑树;
红黑树上,按到期时间进行排序,最先到期的hrtimer位于最左下的节点,并被记录在active.next字段中;
3中时间基准的最先到期时间可能不同,所以,它们之中最先到期的时间被记录在hrtimer_cpu_base的expires_next字段中。

总结

基于以上分析做出总结,

Linux 时间子系统 最重要的就是clockevent_device 无论是否使能高精度定时器都需要在mach层实现一个clockevent_device ,

在没有使能HRTIMER的情况下下这个clockevent_device 的中断每 1/HZ 触发一次 ,在触发的中断中调用clockevent_device 中的event_handle 在这种情况下系统将event_handle 设置为 tick_handle_periodic , 每调用一次他 就可以为 jiffies+1 从而驱动系统默认的clocksource设备。此外在tick_handle_periodic 会通过时间轮算法结合软中断驱动time_list设备的运行。

而在使能HRTIMER的情况下,开始也是处于低精度模式,clockevent_device的event_handle 依然设置为 tick_handle_periodic,但是相比没有使能HRTIMER的情况下, 在运行软中断中会运行hrtimer_run_queues, 通过这种方式,使得即使没有开启高精度模式依然可以使用hrtimer接口函数,但是这分辨率还是只有HZ,同时在每次tick中还会使用hrtimer_run_pend 查询当前是否满足切换到高精度模式的条件,首先通过检查标志位判断当前是否有新的时钟设备插入,仅仅只有检测到时钟设备的插入(在插入的时候对一个位进行置位)然后检测 当前tick_device是否支持one shot模式 还有就是当前timerkeeper是否支持高精度模式 。在发现支持切换到高精度模式之后,系统就会将

event_handle 设置为hrtimer_interupt 同时将tick_device 设置为one-shot模式,这个时候才算真正启动了高精度定时器。

在实际编程的过程中一开始 我仅仅通过定时器实现了一个clockevent_device ,但是发现始终无法切换到高精度模式,在阅读了源码之后发现其在切换过程中被卡在了对于timekeeper的检测上,hrtimer的工作模式就是在每次interupt中 检测hrtimer 到期时间和 timerkeeper中所保存的墙上时间的差值来设定下一次定时器到期中断的装载值,所以可以想象如果没有自己实现clocksource 而依然使用系统默认的jiffies , 一定是不行的,hrtimer的作用之一就是驱动这个默认的clocksource,这个clocksource的时间数值根本无法为hrtimer设置下一次装载值提供准确的时间。

编程

所以为了实现HRTIMER 必须基于芯片中的定时器分别实现一个clockevent_device和一个clocksource_device 这意味这至少需要两个以上的定时器才可以实现。

#include <linux/clockchips.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/module.h>
#include <asm/time.h>
#include <asm/bitops.h>
#include "bsp_ioal.h"
#include "bspchip.h"

#define DIVISOR_SET           10

#define FREQUENCE             1000

#define RTL9300_CLOCK_RATE   ((MHZ_9300_MP * 1000000)/ ((int) DIVISOR_SET ))
#define RTL9300_SECOND_LOAD  RTL9300_CLOCK_RATE

static struct clock_event_device hrtimer_event_device;

static int timer_set_next_event(unsigned long delta,
                 struct clock_event_device *evt)
{
    BSP_REG32(RTL93XXMP_TC3INT) |= RTL93XXMP_TCIP;
    BSP_REG32(RTL93XXMP_TC3DATA ) =  delta;
    BSP_REG32(RTL93XXMP_TC3CNT ) = 0;
    BSP_REG32(RTL93XXMP_TC3INT) = RTL93XXMP_TCIE;
    return 0;
}

static void timer_set_mode(enum clock_event_mode mode , struct clock_event_device *evt)
{
    printk("timer_set_mode %s %d \n" ,evt->name, mode);
    return;
}

static void timer_event_handler(struct clock_event_device *evt)
{
    printk("timer_event_handler\n");
}

void calc_mult_shift(u32 *mult, u32 *shift, u32 from, u32 to, u32 maxsec)
{
	u64 tmp;
	u32 sft, sftacc= 32;
	tmp = ((u64)maxsec * from) >> 32;
	while (tmp) {
		tmp >>=1;
		sftacc--;
	}
	for (sft = 32; sft > 0; sft--) {
		tmp = (u64) to << sft;
		tmp += from / 2;
		do_div(tmp, from);
		if ((tmp >> sftacc) == 0)
			break;
	}
	*mult = tmp;
	*shift = sft;
}

static irqreturn_t evt_timer_interrupt(int irq, void *dev_id)
{
    struct clock_event_device *cd = &hrtimer_event_device;
    BSP_REG32(RTL93XXMP_TC3INT) |= RTL93XXMP_TCIP;
    BSP_REG32(RTL93XXMP_TC3INT) &= ~RTL93XXMP_TCIE;
    cd->event_handler(cd);
    return IRQ_HANDLED;
}

static struct irqaction evt_timer_irqaction = {
    .handler	= evt_timer_interrupt,
    .flags		= IRQF_TIMER ,
    .name		= "HRTTIMER_IRQ",
};

int hrtimer_clockevent_init(void)
{
    struct clock_event_device *cd;

    cd = &hrtimer_event_device;

    cd->name		= "HRTTIMER_ENENT";
    cd->features	= CLOCK_EVT_FEAT_ONESHOT ;

    cd->event_handler	= timer_event_handler;
    cd->set_next_event	= timer_set_next_event;
    cd->set_mode	= timer_set_mode;

    cd->rating = 300;
    cd->irq = BSP_IRQ_ICTL_BASE+TC3_IRQ;
    cd->cpumask = cpu_all_mask;

    calc_mult_shift(&cd->mult, &cd->shift, NSEC_PER_SEC, RTL9300_CLOCK_RATE, 4);

    printk("clockevent :%d %d \n" , cd->mult ,cd->shift);
    printk("1 cycle %llu ns\n" , clockevent_delta2ns(1, cd));

    cd->max_delta_ns = clockevent_delta2ns(0xfffffff, cd);
    cd->min_delta_ns = clockevent_delta2ns(0xf, cd);

    BSP_REG32(RTL93XXMP_TC3CTL) =  RTL93XXMP_TCEN| RTL93XXMP_TCMODE_TIMER | DIVISOR_SET ;

    clockevents_register_device(cd);
    setup_irq(BSP_IRQ_ICTL_BASE+TC3_IRQ, &evt_timer_irqaction);

    return 0;
}

cycle_t hrtimer_get_cycles(struct clocksource *cs)
{ 
    return BSP_REG32(RTL93XXMP_TC4CNT);
}

static struct clocksource hrtimer_clocksource = {
	.name	= "HRTIMER_SOURCE",
	.rating = 300,
	.read	= hrtimer_get_cycles,
	.mask	= 0xfffffff,
	.flags	= CLOCK_SOURCE_IS_CONTINUOUS ,
};

void hrtimer_clocksource_init(void)
{
    BSP_REG32(RTL93XXMP_TC4CTL) = 0; 
    BSP_REG32(RTL93XXMP_TC4DATA ) =  0xfffffff ;
    BSP_REG32(RTL93XXMP_TC4CTL) =  RTL93XXMP_TCEN| RTL93XXMP_TCMODE_TIMER | DIVISOR_SET ;

    calc_mult_shift(&hrtimer_clocksource.mult, &hrtimer_clocksource.shift, RTL9300_CLOCK_RATE , NSEC_PER_SEC, 4);

    printk("clocksource:%lu %lu load 0x%lx\n" , hrtimer_clocksource.mult ,hrtimer_clocksource.shift,BSP_REG32(RTL93XXMP_TC4DATA));
    printk("1 cycle %llu ns \n" ,((u64) 1 * hrtimer_clocksource.mult) >> hrtimer_clocksource.shift);

	clocksource_register(&hrtimer_clocksource);
}

static int __init timer_init(void)
{
    hrtimer_clocksource_init();
    hrtimer_clockevent_init();

    return 0;
}


static void __exit timer_exit(void)
{
    remove_irq(BSP_IRQ_ICTL_BASE+TC3_IRQ, &evt_timer_irqaction);
    clocksource_unregister(&hrtimer_clocksource);
}

module_init(timer_init);
module_exit(timer_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("YURI");

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/59272.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

从 Nauty 数据结构出发认识群论

在阅读本文前&#xff0c;强烈建议有志入门群论的同学观看 3blue1brown 魔群 视频。 对于计算机方向同学&#xff0c;可以尝试从数据结构的角度理解。本文主要基于文档、网站 Nauty 和 Nauty 的 python binding, pynauty(github.com) 展开。 Nauty 数据结构 本小节截选自 Na…

字节跳动虚拟数字人技术与应用

导读&#xff1a;火山引擎正在打造完善的虚拟数字人技术和应用体系&#xff0c;那么火山引擎是如何定义虚拟数字人的呢&#xff1f;火山引擎 2D 虚拟数字人和 3D 数字人采用了怎样先进的技术&#xff1f;火山引擎数字人有哪些应用和前景展望&#xff1f;今天我们就来一起探秘火…

[附源码]计算机毕业设计基于SpringBoot的毕业生就业系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

浅谈操作系统和进程

前言 操作系统是一个软件&#xff0c;对下要管理硬件设备&#xff0c;对上要给软件运行提供稳定的运行环境。操作系统是软硬件及用户之间交互的媒介。最常见的操作系统有Windows 98&#xff0c;2000&#xff0c;xp&#xff0c;vista&#xff0c;win7&#xff0c;win8&#xff…

# 智慧社区管理系统-核心业务功能-04保修信息

一 后端 1:entity package com.woniu.community.entity;import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor;Data AllArgsConstructor NoArgsConstructor public class Repair {private int id;private String comId;private String co…

[附源码]计算机毕业设计springboot医院挂号住院管理系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

LeetCode 441. 排列硬币

&#x1f308;&#x1f308;&#x1f604;&#x1f604; 欢迎来到茶色岛独家岛屿&#xff0c;本期将为大家揭晓LeetCode 441. 排列硬币 &#xff0c;做好准备了么&#xff0c;那么开始吧。 &#x1f332;&#x1f332;&#x1f434;&#x1f434; 一、题目名称 LeetCode 441. …

Java基于springboot+vue的游戏物品销售购物商城系统 前后端分离

随着时代和计算机的发展&#xff0c;出现了越来越多的网络游戏&#xff0c;相对应的也拥有了越来越多的玩家&#xff0c;这些玩家在玩了一段游戏之后&#xff0c;可能会有游戏交易的需求。如果直接在私下个人交易很不安全&#xff0c;容易被骗。为了能够让广大游戏爱好者拥有一…

.net6 web api中使用SqlSugar(MySQL数据库)

其中SqlSugar&#xff0c;也可以是EFcore&#xff0c;或者Dapper&#xff0c;或者其他ORM框架。 其中mysql&#xff0c;也可以是SqlServer&#xff0c;或者oracle&#xff0c;或者其他数据库类型。 1.首先使用vs2022建立.net6 web api 2.增加SqlSugar和MySQL依赖项。 Newton…

17.前端笔记-CSS-定位

1、为什么要定位 先看看以下这些场景是否可以用标准流或浮动实现 &#xff08;1&#xff09;某个元素可以自由的在一个盒子内移动位置&#xff0c;并且压住其他盒子 &#xff08;2&#xff09;滚动窗口时&#xff0c;某些盒子是可以固定在屏幕某个位置的 以上这种场景使用标准…

# 智慧社区管理系统-核心业务管理-01车位收费

一 后端 1:entity package com.woniu.community.entity;import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor;Data AllArgsConstructor NoArgsConstructor public class CarCharge {private int id;private String payDate;//开始时间pr…

Vue 官方文档2.x教程学习笔记 1 基础 1.6 Class 与 Style 绑定 1.6.1 绑定HTML Class

Vue 官方文档2.x教程学习笔记 文章目录Vue 官方文档2.x教程学习笔记1 基础1.6 Class 与 Style 绑定1.6.1 绑定HTML Class1 基础 1.6 Class 与 Style 绑定 操作元素的 class 列表和内联样式是数据绑定的一个常见需求。 因为它们都是 attribute&#xff0c;所以我们可以用 v-b…

浅析Vue3动态组件怎么进行异常处理

Vue3动态组件怎么进行异常处理&#xff1f;下面本篇文章带大家聊聊Vue3 动态组件异常处理的方法&#xff0c;希望对大家有所帮助&#xff01; 动态组件有两种常用场景&#xff1a; 一是动态路由&#xff1a; // 动态路由 export const asyncRouterMap: Array<RouteRecordR…

【算法】山东大学人工智能限选课实验一(八数码问题)

实验一 八数码问题 1. 题目介绍 八数码问题描述为&#xff1a;在 33 组成的九宫格棋盘上&#xff0c;摆有 8 张牌&#xff0c;每张牌都刻有 1-8 中的某一个数码。棋盘中留有一个空格&#xff0c;允许其周围的某张牌向空格移动&#xff0c;这样通过移动牌就可以不断改变棋盘布…

【实验报告NO.000001】MIT 6.858 Computer System Security - Lab 1

0x00. 一切开始之前 MIT 6.858 是面向高年级本科生与研究生开设的一门关于计算机系统安全&#xff08;secure computer security&#xff09;的课程&#xff0c;内容包括威胁模型&#xff08;threat models&#xff09;、危害安全的攻击&#xff08;attacks that compromise s…

客快物流大数据项目(九十):ClickHouse的引擎介绍和深入日志引擎讲解

文章目录 ClickHouse的引擎介绍和深入日志引擎讲解 一、引擎介绍 二、日志引擎

【大数据趋势】12月3日纳指大概率反弹到黄金分割附近,然后下跌,之后进入趋势选择期,恒指会跟随。感觉或许有什么大事情要发生,瞎猜中。

行情核心源头分析: 纳斯达克指数 是否会符合大数据规则&#xff0c;走黄金分割线规则 回顾一下上周大数据预测的趋势&#xff0c;虽有波折但最终趋势预测准确 上周11.20日大数据模拟出一个趋势图&#xff0c;大趋势上需要继续上涨尾期&#xff0c;制造一个背离出现&#xff0c…

ZMQ中请求-应答模式的可靠性设计

一、什么是可靠性&#xff1f; 要给可靠性下定义&#xff0c;我们可以先界定它的相反面——故障。如果我们可以处理某些类型的故障&#xff0c;那么我们的模型对于这些故障就是可靠的。下面我们就来列举分布式ZMQ应用程序中可能发生的问题&#xff0c;从可能性高的故障开始&…

[附源码]计算机毕业设计springboot演唱会门票售卖系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

总结:原生servlet请求转发url与请求重定向url的使用区别

总结&#xff1a;原生servlet请求转发url与请求重定向url的使用区别一演示前提&#xff1a;1.演示案例的项目架构如图&#xff1a;2.设置web应用的映射根目录&#xff1a;/lmf&#xff0c;当然也可以不设置。二什么叫请求转发、请求重定向&#xff1f;1.请求转发解释图2. forwa…