<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
	<channel>
		<atom:link href="http://gentoo-zh.org/extern.php?action=feed&amp;tid=862&amp;type=rss" rel="self" type="application/rss+xml" />
		<title><![CDATA[Gentoo中文社区 / Gentoo时间子系统之四：定时器的引擎：clock_event_device]]></title>
		<link>http://www.gentoo-zh.org/viewtopic.php?id=862</link>
		<description><![CDATA[Gentoo时间子系统之四：定时器的引擎：clock_event_device 最近发表的帖子。]]></description>
		<lastBuildDate>Tue, 23 Apr 2024 12:43:52 +0000</lastBuildDate>
		<generator>FluxBB</generator>
		<item>
			<title><![CDATA[Gentoo时间子系统之四：定时器的引擎：clock_event_device]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?pid=981#p981</link>
			<description><![CDATA[<p>早期的内核版本中，进程的调度基于一个称之为tick的时钟滴答，通常使用时钟中断来定时地产生tick信号，每次tick定时中断都会进行进程的统计和调度，并对tick进行计数，记录在一个jiffies变量中，定时器的设计也是基于jiffies。这时候的内核代码中，几乎所有关于时钟的操作都是在machine级的代码中实现，很多公共的代码要在每个平台上重复实现。随后，随着通用时钟框架的引入，内核需要支持高精度的定时器，为此，通用时间框架为定时器硬件定义了一个标准的接口：clock_event_device，machine级的代码只要按这个标准接口实现相应的硬件控制功能，剩下的与平台无关的特性则统一由通用时间框架层来实现。</p><p>1.&#160; 时钟事件软件架构<br />本系列文章的第一节中，我们曾经讨论了时钟源设备：clocksource，现在又来一个时钟事件设备：clock_event_device，它们有何区别？看名字，好像都是给系统提供时钟的设备，实际上，clocksource不能被编程，没有产生事件的能力，它主要被用于timekeeper来实现对真实时间进行精确的统计，而clock_event_device则是可编程的，它可以工作在周期触发或单次触发模式，系统可以对它进行编程，以确定下一次事件触发的时间，clock_event_device主要用于实现普通定时器和高精度定时器，同时也用于产生tick事件，供给进程调度子系统使用。时钟事件设备与通用时间框架中的其他模块的关系如下图所示： </p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/04/2901284100.png" alt="FluxBB bbcode 测试" /></span></p><p>&gt;与clocksource一样，系统中可以存在多个clock_event_device，系统会根据它们的精度和能力，选择合适的clock_event_device对系统提供时钟事件服务。在smp系统中，为了减少处理器间的通信开销，基本上每个cpu都会具备一个属于自己的本地clock_event_device，独立地为该cpu提供时钟事件服务，smp中的每个cpu基于本地的clock_event_device，建立自己的tick_device，普通定时器和高精度定时器。<br />&gt;在软件架构上看，clock_event_device被分为了两层，与硬件相关的被放在了machine层，而与硬件无关的通用代码则被集中到了通用时间框架层，这符合内核对软件的设计需求，平台的开发者只需实现平台相关的接口即可，无需关注复杂的上层时间框架。<br />&gt;tick_device是基于clock_event_device的进一步封装，用于代替原有的时钟滴答中断，给内核提供tick事件，以完成进程的调度和进程信息统计，负载平衡和时间更新等操作。</p><p>2.&#160; 时钟事件设备相关数据结构<br />2.1&#160; struct clock_event_device</p><p>时钟事件设备的核心数据结构是clock_event_device结构，它代表着一个时钟硬件设备，该设备就好像是一个具有事件触发能力（通常就是指中断）的clocksource，它不停地计数，当计数值达到预先编程设定的数值那一刻，会引发一个时钟事件中断，继而触发该设备的事件处理回调函数，以完成对时钟事件的处理。clock_event_device结构的定义如下：</p><div class="codebox"><pre><code>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;</code></pre></div><p>event_handler&#160; 该字段是一个回调函数指针，通常由通用框架层设置，在时间中断到来时，machine底层的的中断服务程序会调用该回调，框架层利用该回调实现对时钟事件的处理。</p><p>set_next_event&#160; 设置下一次时间触发的时间，使用类似于clocksource的cycle计数值（离现在的cycle差值）作为参数。</p><p>set_next_ktime&#160; 设置下一次时间触发的时间，直接使用ktime时间作为参数。</p><p>max_delta_ns&#160; 可设置的最大时间差，单位是纳秒。</p><p>min_delta_ns&#160; 可设置的最小时间差，单位是纳秒。</p><p>mult shift&#160; 与clocksource中的类似，只不过是用于把纳秒转换为cycle。</p><p>mode&#160; 该时钟事件设备的工作模式，两种主要的工作模式分别是：</p><p>&gt;CLOCK_EVT_MODE_PERIODIC&#160; 周期触发模式，设置后按给定的周期不停地触发事件；<br />&gt;CLOCK_EVT_MODE_ONESHOT&#160; 单次触发模式，只在设置好的触发时刻触发一次；</p><p>set_mode&#160; 函数指针，用于设置时钟事件设备的工作模式。</p><p>rating&#160; 表示该设备的精度等级。</p><p>list&#160; 系统中注册的时钟事件设备用该字段挂在全局链表变量clockevent_devices上。<br />2.2&#160; 全局变量clockevent_devices<br />系统中所有注册的clock_event_device都会挂在该链表下面，它在kernel/time/clockevents.c中定义： </p><p>&gt;static LIST_HEAD(clockevent_devices);</p><p>2.3&#160; 全局变量clockevents_chain<br />通用时间框架初始化时会注册一个通知链（NOTIFIER），当系统中的时钟时间设备的状态发生变化时，利用该通知链通知系统的其它模块。 </p><div class="codebox"><pre><code>/* Notification for clock events */
static RAW_NOTIFIER_HEAD(clockevents_chain);</code></pre></div><p>3.&#160; clock_event_device的初始化和注册<br />每一个machine，都要定义一个自己的machine_desc结构，该结构定义了该machine的一些最基本的特性，其中需要设定一个sys_timer结构指针，machine级的代码负责定义sys_timer结构，sys_timer的声明很简单：</p><div class="codebox"><pre><code>struct sys_timer {
	void			(*init)(void);
	void			(*suspend)(void);
	void			(*resume)(void);
#ifdef CONFIG_ARCH_USES_GETTIMEOFFSET
	unsigned long		(*offset)(void);
#endif
};</code></pre></div><p>通常，我们至少要定义它的init字段，系统初始化阶段，该init回调会被调用，该init回调函数的主要作用就是完成系统中的clocksource和clock_event_device的硬件初始化工作，以samsung的exynos4为例，在V3.4内核的代码树中，machine_desc的定义如下： </p><div class="codebox"><pre><code>MACHINE_START(SMDK4412, &quot;SMDK4412&quot;)
	/* Maintainer: Kukjin Kim &lt;kgene.kim@samsung.com&gt; */
	/* Maintainer: Changhwan Youn &lt;chaos.youn@samsung.com&gt; */
	.atag_offset	= 0x100,
	.init_irq	= exynos4_init_irq,
	.map_io		= smdk4x12_map_io,
	.handle_irq	= gic_handle_irq,
	.init_machine	= smdk4x12_machine_init,
	.timer		= &amp;exynos4_timer,
	.restart	= exynos4_restart,
MACHINE_END</code></pre></div><p>定义的sys_timer是exynos4_timer，它的定义和init回调定义如下： </p><div class="codebox"><pre><code>static void __init exynos4_timer_init(void)
{
	if (soc_is_exynos4210())
		mct_int_type = MCT_INT_SPI;
	else
		mct_int_type = MCT_INT_PPI;
 
	exynos4_timer_resources();
	exynos4_clocksource_init();
	exynos4_clockevent_init();
}
 
struct sys_timer exynos4_timer = {
	.init		= exynos4_timer_init,
};</code></pre></div><p>exynos4_clockevent_init函数显然是初始化和注册clock_event_device的合适时机，在这里，它注册了一个rating为250的clock_event_device，并把它指定给cpu0： </p><div class="codebox"><pre><code>static struct clock_event_device mct_comp_device = {
	.name		= &quot;mct-comp&quot;,
	.features       = CLOCK_EVT_FEAT_PERIODIC | CLOCK_EVT_FEAT_ONESHOT,
	.rating		= 250,
	.set_next_event	= exynos4_comp_set_next_event,
	.set_mode	= exynos4_comp_set_mode,
};
......
static void exynos4_clockevent_init(void)
{
	clockevents_calc_mult_shift(&amp;mct_comp_device, clk_rate, 5);
        ......
	mct_comp_device.cpumask = cpumask_of(0);
	clockevents_register_device(&amp;mct_comp_device);
 
	setup_irq(EXYNOS4_IRQ_MCT_G0, &amp;mct_comp_event_irq);
}</code></pre></div><p>因为这个阶段其它cpu核尚未开始工作，所以该clock_event_device也只是在启动阶段给系统提供服务，实际上，因为exynos4是一个smp系统，具备2-4个cpu核心，前面说过，smp系统中，通常会使用各个cpu的本地定时器来为每个cpu单独提供时钟事件服务，继续翻阅代码，在系统初始化的后段，kernel_init会被调用，它会调用smp_prepare_cpus，其中会调用percpu_timer_setup函数，在arch/arm/kernel/smp.c中，为每个cpu定义了一个clock_event_device： </p><div class="codebox"><pre><code>/*
 * Timer (local or broadcast) support
 */
static DEFINE_PER_CPU(struct clock_event_device, percpu_clockevent);</code></pre></div><p>percpu_timer_setup最终会调用exynos4_local_timer_setup函数完成对本地clock_event_device的初始化工作： </p><div class="codebox"><pre><code>static int __cpuinit exynos4_local_timer_setup(struct clock_event_device *evt)
{
    ......
	evt-&gt;name = mevt-&gt;name;
	evt-&gt;cpumask = cpumask_of(cpu);
	evt-&gt;set_next_event = exynos4_tick_set_next_event;
	evt-&gt;set_mode = exynos4_tick_set_mode;
	evt-&gt;features = CLOCK_EVT_FEAT_PERIODIC | CLOCK_EVT_FEAT_ONESHOT;
	evt-&gt;rating = 450;
 
	clockevents_calc_mult_shift(evt, clk_rate / (TICK_BASE_CNT + 1), 5);
    ......
	clockevents_register_device(evt);
    ......
	enable_percpu_irq(EXYNOS_IRQ_MCT_LOCALTIMER, 0);
    ......
	return 0;
}</code></pre></div><p>由此可见，每个cpu的本地clock_event_device的rating是450，比启动阶段的250要高，显然，之前注册给cpu0的精度要高，系统会用本地clock_event_device替换掉原来分配给cpu0的clock_event_device，至于怎么替换？我们先停一停，到这里我们一直在讨论machine级别的初始化和注册，让我们回过头来，看看框架层的初始化。在继续之前，让我们看看整个clock_event_device的初始化的调用序列图： </p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/04/2467323694.png" alt="FluxBB bbcode 测试" /></span></p><p>由上面的图示可以看出，框架层的初始化步骤很简单，又start_kernel开始，调用tick_init，它位于kernel/time/tick-common.c中，也只是简单地调用clockevents_register_notifier，同时把类型为notifier_block的tick_notifier作为参数传入，回看2.3节，clockevents_register_notifier注册了一个通知链，这样，当系统中的clock_event_device状态发生变化时（新增，删除，挂起，唤醒等等），tick_notifier中的notifier_call字段中设定的回调函数tick_notify就会被调用。接下来start_kernel调用了time_init函数，该函数通常定义在体系相关的代码中，正如前面所讨论的一样，它主要完成machine级别对时钟系统的初始化工作，最终通过clockevents_register_device注册系统中的时钟事件设备，把每个时钟时间设备挂在clockevent_device全局链表上，最后通过clockevent_do_notify触发框架层事先注册好的通知链，其实就是调用了tick_notify函数，我们主要关注CLOCK_EVT_NOTIFY_ADD通知，其它通知请自行参考代码，下面是tick_notify的简化版本：</p><div class="codebox"><pre><code>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;
}</code></pre></div><p>可见，对于新注册的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和高精度定时器提供服务，这些内容我们留在本系列的后续文章进行讨论。</p><p>4.&#160; tick_device<br />当内核没有配置成支持高精度定时器时，系统的tick由tick_device产生，tick_device其实是clock_event_device的简单封装，它内嵌了一个clock_event_device指针和它的工作模式： </p><div class="codebox"><pre><code>struct tick_device {
	struct clock_event_device *evtdev;
	enum tick_device_mode mode;
};</code></pre></div><p>在kernel/time/tick-common.c中，定义了一个per-cpu的tick_device全局变量，tick_cpu_device： </p><div class="codebox"><pre><code>/*
 * Tick devices
 */
DEFINE_PER_CPU(struct tick_device, tick_cpu_device);</code></pre></div><p>前面曾经说过，当machine的代码为每个cpu注册clock_event_device时，通知回调函数tick_notify会被调用，进而进入tick_check_new_device函数，下面让我们看看该函数如何工作，首先，该函数先判断注册的clock_event_device是否可用于本cpu，然后从per-cpu变量中取出本cpu的tick_device： </p><div class="codebox"><pre><code>static int tick_check_new_device(struct clock_event_device *newdev)
{
        ......
	cpu = smp_processor_id();
	if (!cpumask_test_cpu(cpu, newdev-&gt;cpumask))
		goto out_bc;
 
	td = &amp;per_cpu(tick_cpu_device, cpu);
	curdev = td-&gt;evtdev;</code></pre></div><p>如果不是本地clock_event_device，会做进一步的判断：如果不能把irq绑定到本cpu，则放弃处理，如果本cpu已经有了一个本地clock_event_device，也放弃处理：</p><div class="codebox"><pre><code>	if (!cpumask_equal(newdev-&gt;cpumask, cpumask_of(cpu))) {
                ......
		if (!irq_can_set_affinity(newdev-&gt;irq))
			goto out_bc;
                ......
		if (curdev &amp;&amp; cpumask_equal(curdev-&gt;cpumask, cpumask_of(cpu)))
			goto out_bc;
	}</code></pre></div><p>反之，如果本cpu已经有了一个clock_event_device，则根据是否支持单触发模式和它的rating值，决定是否替换原来旧的clock_event_device： </p><div class="codebox"><pre><code>	if (curdev) {
		if ((curdev-&gt;features &amp; CLOCK_EVT_FEAT_ONESHOT) &amp;&amp;
		    !(newdev-&gt;features &amp; CLOCK_EVT_FEAT_ONESHOT))
			goto out_bc;  // 新的不支持单触发，但旧的支持，所以不能替换
		if (curdev-&gt;rating &gt;= newdev-&gt;rating)
			goto out_bc;  // 旧的比新的精度高，不能替换
	}</code></pre></div><p>在这些判断都通过之后，说明或者来cpu还没有绑定tick_device，或者是新的更好，需要替换： </p><div class="codebox"><pre><code>	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));</code></pre></div><p>上面的tick_setup_device函数负责重新绑定当前cpu的tick_device和新注册的clock_event_device，如果发现是当前cpu第一次注册tick_device，就把它设置为TICKDEV_MODE_PERIODIC模式，如果是替换旧的tick_device，则根据新的tick_device的特性，设置为TICKDEV_MODE_PERIODIC或TICKDEV_MODE_ONESHOT模式。可见，在系统的启动阶段，tick_device是工作在周期触发模式的，直到框架层在合适的时机，才会开启单触发模式，以便支持NO_HZ和HRTIMER。<br />5.&#160; tick事件的处理--最简单的情况<br />clock_event_device最基本的应用就是实现tick_device，然后给系统定期地产生tick事件，通用时间框架对clock_event_device和tick_device的处理相当复杂，因为涉及配置项：CONFIG_NO_HZ和CONFIG_HIGH_RES_TIMERS的组合，两个配置项就有4种组合，这四种组合的处理都有所不同，所以这里我先只讨论最简单的情况：</p><p>&gt;CONFIG_NO_HZ == 0;<br />&gt;CONFIG_HIGH_RES_TIMERS == 0;</p><p>在这种配置模式下，我们回到上一节的tick_setup_device函数的最后： </p><div class="codebox"><pre><code>	if (td-&gt;mode == TICKDEV_MODE_PERIODIC)
		tick_setup_periodic(newdev, 0);
	else
		tick_setup_oneshot(newdev, handler, next_event);</code></pre></div><p>因为启动期间，第一个注册的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即可： </p><div class="codebox"><pre><code>void tick_handle_periodic(struct clock_event_device *dev)
{
	int cpu = smp_processor_id();
	ktime_t next;
 
	tick_periodic(cpu);
 
	if (dev-&gt;mode != CLOCK_EVT_MODE_ONESHOT)
		return;
 
	next = ktime_add(dev-&gt;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);
	}
}</code></pre></div><p>该函数首先调用tick_periodic函数，完成tick事件的所有处理，如果是周期触发模式，处理结束，如果工作在单触发模式，则计算并设置下一次的触发时刻，这里用了一个循环，是为了防止当该函数被调用时，clock_event_device中的计时实际上已经经过了不止一个tick周期，这时候，tick_periodic可能被多次调用，使得jiffies和时间可以被正确地更新。tick_periodic的代码如下： </p><div class="codebox"><pre><code>static void tick_periodic(int cpu)
{
	if (tick_do_timer_cpu == cpu) {
		write_seqlock(&amp;xtime_lock);
 
		/* Keep track of the next tick event */
		tick_next_period = ktime_add(tick_next_period, tick_period);
 
		do_timer(1);
		write_sequnlock(&amp;xtime_lock);
	}
 
	update_process_times(user_mode(get_irq_regs()));
	profile_tick(CPU_PROFILING);
}</code></pre></div><p>如果当前cpu负责更新时间，则通过do_timer进行以下操作：<br />&gt;更新jiffies_64变量；<br />&gt;更新墙上时钟；<br />&gt;每10个tick，更新一次cpu的负载信息；</p><p>调用update_peocess_times，完成以下事情：<br />&gt;更新进程的时间统计信息；<br />&gt;触发TIMER_SOFTIRQ软件中断，以便系统处理传统的低分辨率定时器；<br />&gt;检查rcu的callback；<br />&gt;通过scheduler_tick触发调度系统进行进程统计和调度工作；</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Tue, 23 Apr 2024 12:43:52 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?pid=981#p981</guid>
		</item>
	</channel>
</rss>
