Linux时间子系统之Tick广播层(Tick Broadcast)

在分析Tick模拟层的时候曾经提到过,当系统中没有别的进程需要处理的时候,会将当前CPU切换到NO_HZ状态,不会每一个Tick都收到定时中断,从而达到节电的目的。但此时,当前CPU上的定时事件设备还是打开的,处于工作状态,只不过不产生Tick了。但是,如果当前CPU上的定时事件设备还支持一种叫做C3_STOP的状态的话,有可能当CPU进入某些空闲状态的时候,连为本CPU服务的定时事件设备都会被完全停止掉。这时候,本CPU将完全接收不到任何定时中断,也不会自己把自己唤醒,必须寻求外部设备或其它没有休眠CPU的帮助,这就是Tick广播层存在的目的。

在Tick广播层,定义了如下六个全局变量:

static cpumask_var_t tick_broadcast_mask __cpumask_var_read_mostly;
static cpumask_var_t tick_broadcast_on __cpumask_var_read_mostly;
static cpumask_var_t tmpmask __cpumask_var_read_mostly;
......
#ifdef CONFIG_TICK_ONESHOT

static cpumask_var_t tick_broadcast_oneshot_mask __cpumask_var_read_mostly;
static cpumask_var_t tick_broadcast_pending_mask __cpumask_var_read_mostly;
static cpumask_var_t tick_broadcast_force_mask __cpumask_var_read_mostly;
......
#endif /* CONFIG_TICK_ONESHOT */
  • tick_broadcast_mask:该变量中的每一位表示对应的CPU是否需要Tick广播层提供Tick周期广播服务,如果需要则对应的位被置位。
  • tick_broadcast_on:这个变量是用来控制打开或关闭Tick周期广播服务的开关,如果当前CPU有可能会进入深度休眠状态,则对应该CPU的位会被置位。
  • tmpmask:临时变量。
  • tick_broadcast_oneshot_mask:当Tick广播层被切换到单次触发模式后,用来记录哪些CPU已经进入的深度休眠模式,也就是本地定时事件设备被关闭了,需要Tick广播层提供服务。
  • tick_broadcast_pending_mask和tick_broadcast_force_mask:用来处理特殊的竞态情况,后面会解释。

这些全局变量都是在tick_broadcast_init函数里面初始化的:

void __init tick_broadcast_init(void)
{
	zalloc_cpumask_var(&tick_broadcast_mask, GFP_NOWAIT);
	zalloc_cpumask_var(&tick_broadcast_on, GFP_NOWAIT);
	zalloc_cpumask_var(&tmpmask, GFP_NOWAIT);
#ifdef CONFIG_TICK_ONESHOT
	zalloc_cpumask_var(&tick_broadcast_oneshot_mask, GFP_NOWAIT);
	zalloc_cpumask_var(&tick_broadcast_pending_mask, GFP_NOWAIT);
	zalloc_cpumask_var(&tick_broadcast_force_mask, GFP_NOWAIT);
#endif
}

可以看到,如果Tick广播层被配置成不支持单次触发模式的话,tick_broadcast_oneshot_mask、tick_broadcast_pending_mask和tick_broadcast_force_mask这三个全局变量是没有用的。

1)Tick广播设备的安装

在介绍Tick层的新设备设置和切换场景时,曾提到过当注册上来一个新的定时事件设备的时候,会调用Tick层的tick_check_new_device函数尝试使用新的定时事件设备替换老的定时事件设备,作为当前CPU上的Tick设备。但是,如果新的设备某些条件不满足或者还不如老的设备的时候,会接着尝试调用tick_install_broadcast_device函数,看看这个设备能不能作为Tick广播层的设备:

void tick_install_broadcast_device(struct clock_event_device *dev)
{
	struct clock_event_device *cur = tick_broadcast_device.evtdev;

        /* 是否可以用新设备替换老设备作为Tick广播设备 */
	if (!tick_check_broadcast_device(cur, dev))
		return;

        /* 对应的驱动模块存不存在 */
	if (!try_module_get(dev->owner))
		return;

        /* 更换当前定时事件设备 */
	clockevents_exchange_device(cur, dev);
	if (cur)
                /* 将老定时事件设备的event_handler设置成什么都不做的函数 */
		cur->event_handler = clockevents_handle_noop;
        /* 用新设备替换当前的Tick广播设备 */
	tick_broadcast_device.evtdev = dev;
        /* 当前系统中是否已经有CPU需要提供Tick广播服务了 */
	if (!cpumask_empty(tick_broadcast_mask))
		tick_broadcast_start_periodic(dev);
	/* 通知Tick模拟层有新的定时事件设备注册上来 */
	if (dev->features & CLOCK_EVT_FEAT_ONESHOT)
		tick_clock_notify();
}

tick_check_broadcast_device函数的主要功能是检查和比较新老定时事件设备,看看新的能不能替换老的作为Tick广播设备:

static bool tick_check_broadcast_device(struct clock_event_device *curdev,
					struct clock_event_device *newdev)
{
	if ((newdev->features & CLOCK_EVT_FEAT_DUMMY) ||
	    (newdev->features & CLOCK_EVT_FEAT_PERCPU) ||
	    (newdev->features & CLOCK_EVT_FEAT_C3STOP))
		return false;

	if (tick_broadcast_device.mode == TICKDEV_MODE_ONESHOT &&
	    !(newdev->features & CLOCK_EVT_FEAT_ONESHOT))
		return false;

	return !curdev || newdev->rating > curdev->rating;
}

可以看出来,不是任何定时事件设备都有资格作为系统的Tick广播设备的。这个设备不能是一个假的(CLOCK_EVT_FEAT_DUMMY),不能是和某个CPU绑定的私有设备(CLOCK_EVT_FEAT_PERCPU),不能是支持C3_STOP状态的设备(CLOCK_EVT_FEAT_C3STOP)。如果这个设备本身也是支持C3_STOP状态的,也就意味着该设备也会被彻底停止掉,肯定也就不能向其它设备广播了。还有,如果当前作为系统广播的Tick设备已经切换到了单次触发模式了,而新加入的定时事件设备并不支持单次触发模式,则新设备也没有资格替换老的设备。最后,即使前面的条件都满足了,还要比较定时事件设备的精度值。

如果一切条件都满足了,那就可以正式调用定时事件层的clockevents_exchange_device函数切换了。之后,还要把老的定时事件设备的中断回调函数设置成一个什么都不做的空函数(clockevents_handle_noop),并且把Tick广播设备的定时事件设备替换成新的。

全局变量tick_broadcast_mask表示有没有CPU会请求Tick广播服务。如果当前系统中所有的CPU都不会睡死进入C3_STOP状态,那么即使进入了NO_HZ状态,它们各自维护的定时器也会得到妥善的处理,就不需要Tick广播了。如果tick_broadcast_mask不是0,表明系统中至少有一个设备会完全停止需要Tick广播,那么接下来就需要调用tick_broadcast_start_periodic函数,启动Tick周期广播:

static void tick_broadcast_start_periodic(struct clock_event_device *bc)
{
	if (bc)
		tick_setup_periodic(bc, 1);
}

该函数直接调用了tick_setup_periodic函数:

void tick_setup_periodic(struct clock_event_device *dev, int broadcast)
{
        /* 设置定时事件设备的中断回调函数 */
	tick_set_periodic_handler(dev, broadcast);

	/* 定时事件设备是否可以正常工作 */
	if (!tick_device_is_functional(dev))
		return;

        /* 定时事件设备支持周期触发模式且当前Tick广播层没有切换到单次触发模式 */
	if ((dev->features & CLOCK_EVT_FEAT_PERIODIC) &&
	    !tick_broadcast_oneshot_active()) {
                /* 直接将定时事件设备切换到周期触发模式 */
		clockevents_switch_state(dev, CLOCK_EVT_STATE_PERIODIC);
	} else {
		unsigned int seq;
		ktime_t next;

                /* 读取下一次Tick到来的事件 */
		do {
			seq = read_seqbegin(&jiffies_lock);
			next = tick_next_period;
		} while (read_seqretry(&jiffies_lock, seq));

                /* 将定时事件设备切换成单次触发模式 */
		clockevents_switch_state(dev, CLOCK_EVT_STATE_ONESHOT);

                /* 对定时事件设备进行编程让其在下一次Tick到来时触发中断 */
		for (;;) {
			if (!clockevents_program_event(dev, next, false))
				return;
			next = ktime_add(next, tick_period);
		}
	}
}

该函数首先调用了tick_set_periodic_handler函数来设置定时事件设备的中断回调函数:

void tick_set_periodic_handler(struct clock_event_device *dev, int broadcast)
{
	if (!broadcast)
		dev->event_handler = tick_handle_periodic;
	else
		dev->event_handler = tick_handle_periodic_broadcast;
}

可以看到,如果参数broadcast是真,那定时事件设备的中断回调函数会被设置成tick_handle_periodic_broadcast。

tick_device_is_functional函数用来判断一个定时事件设备是不是可以正常工作:

static inline int tick_device_is_functional(struct clock_event_device *dev)
{
	return !(dev->features & CLOCK_EVT_FEAT_DUMMY);
}

很简单,只是检查了定时事件设备是不是一个“假”的。

tick_setup_periodic函数接下来会看定时事件设备是否支持周期触发模式,并且Tick广播层有没有已经被切换到了单次触发模式。如果定时事件设备支持周期触发模式,并且Tick广播层还没有被切换到单次触发模式,那么简单了,只需要确保定时事件设备工作在周期触发模式就可以了。但是,如果定时事件设备不支持周期触发模式,也就是只支持单次触发模式的话,就需要对其进行编程了,让其在下一个Tick到来的时间点上触发中断。那有没有可能定时事件设备只支持周期触发,而当前Tick广播层现在又处在单次触发模式呢?这种情况是不可能的,这种情况下前面的tick_check_broadcast_device函数在检查的时候就直接把这个定时事件设备淘汰掉了。另外,无论当前Tick广播层处于何种模式下,当有一个新的定时事件设备被选中作为Tick广播设备后,并且当前系统中有CPU需要提供Tick广播服务的情况下,都会将Tick广播层设置到周期触发状态。如果当前Tick层已经被切换到了单次触发状态,又来了一个新的可以替换当前的Tick广播设备,且这时候还有CPU需要提供Tick广播服务,其实会有问题的,但一般用作Tick广播设备的定时事件设备都很早就注册并初始化了,这种情况应该不会发生。

最后还有一个问题,为什么在tick_install_broadcast_device函数最后,在需要安装的新Tick广播设备支持单次触发模式的情况下,还需要调用tick_clock_notify函数通知Tick模拟层有新的定时事件设备注册上来呢?这是因为,在后面切换到单次触发模式的场景下会介绍,每个CPU上即使当前的定时事件设备支持单次触发模式的,但是如果它还支持C3_STOP模式的话,那么当前CPU上的Tick设备切换到单次触发模式的前提是现在Tick广播层的设备必须也要支持单次触发模式,如果不支持就不切换。那么好了,现在来了一个支持单次触发模式的Tick广播设备,虽然在介绍高分辨率定时器层的时候提到,每次Tick到来都会调用tick_check_oneshot_change函数,检查看是否能切换到高精度模式,但是这个函数第一个判断条件就是当前系统中的时钟源设备或定时事件设备有没有改变,如果没有就直接退出了:

int tick_check_oneshot_change(int allow_nohz)
{
	struct tick_sched *ts = this_cpu_ptr(&tick_cpu_sched);
 
        /* 系统中是否已经出现了新的时钟设备 */
	if (!test_and_clear_bit(0, &ts->check_clocks))
		return 0;
 
    ......
}

所以,如果在tick_install_broadcast_device函数最后不通知的话,即使现在所有条件都满足了,Tick层也永远不会切换到单次触发模式。

2)周期触发事件处理

前面一节已经提到过了,Tick广播层挑选出的定时事件设备是有要求的,其中一个非常重要的就是不能是只为某个CPU服务的私有设备,也就是说选出来的设备必须是大家公有的,系统中当前每个在线的CPU都有可能接收到周期中断,包括已经进入空闲状态且私有定时事件设备已经被停止掉的CPU,但是同一个中断只会被一个CPU接收并处理。而且,前面分析也提到了,在周期触发模式下,被Tick广播层选中的定时事件设备的中断回调函数会被设置成tick_handle_periodic_broadcast:

static void tick_handle_periodic_broadcast(struct clock_event_device *dev)
{
        /* 获得属于当前CPU的Tick设备 */
	struct tick_device *td = this_cpu_ptr(&tick_cpu_device);
	bool bc_local;

	raw_spin_lock(&tick_broadcast_lock);

	/* 如果当前Tick广播设备已经被关闭了则直接退出 */
	if (clockevent_state_shutdown(tick_broadcast_device.evtdev)) {
		raw_spin_unlock(&tick_broadcast_lock);
		return;
	}

        /* 发送Tick广播 */
	bc_local = tick_do_periodic_broadcast();

        /* 如果定时事件设备工作在单次触发模式 */
	if (clockevent_state_oneshot(dev)) {
                /* 计算下一次Tick到来的事件 */
		ktime_t next = ktime_add(dev->next_event, tick_period);

                /* 对定时事件设备进行编程 */
		clockevents_program_event(dev, next, true);
	}
	raw_spin_unlock(&tick_broadcast_lock);

	/* 如果本CPU也需要Tick广播服务 */
	if (bc_local)
                /* 直接调用当前CPU上的Tick设备的事件处理函数 */
		td->evtdev->event_handler(td->evtdev);
}

该函数会调用tick_do_periodic_broadcast函数,向可能需要服务的CPU发送广播。不过,如果当前正在处理这个中断的CPU也需要广播服务的话,也就是说Tick广播设备的定时中断搞好激活了某个进入空闲状态的CPU,就没有必要再对自己进行广播了,因此函数最后直接调用属于本CPU的Tick设备的事件处理函数就行了。如果Tick广播使用的定时事件设备工作在单次触发模式,那么还需要对其进行编程,让它在下一个Tick周期到来的时间点上再次触发中断。

static bool tick_do_periodic_broadcast(void)
{
        /* 需要Tick广播服务并且还在线的CPU */
	cpumask_and(tmpmask, cpu_online_mask, tick_broadcast_mask);
	return tick_do_broadcast(tmpmask);
}

tick_broadcast_mask全局变量记录了有哪些CPU需要Tick广播服务,cpu_online_mask全局变量记录了当前系统中有哪些CPU还是在线的,没有被拔掉,一个CPU即使已经进入C3_STOP的“睡死”状态,它仍然还是在线的。该函数计算tick_broadcast_mask和cpu_online_mask的交集,也就是找出需要Tick广播服务并且还在线的CPU,将其存放在tmpmask全局变量中,然后调用tick_do_broadcast函数:

static bool tick_do_broadcast(struct cpumask *mask)
{
	int cpu = smp_processor_id();
	struct tick_device *td;
	bool local = false;

	/* 是否当前CPU也需要Tick广播服务 */
	if (cpumask_test_cpu(cpu, mask)) {
		struct clock_event_device *bc = tick_broadcast_device.evtdev;

                /* 将代表本CPU的标志位清空 */
		cpumask_clear_cpu(cpu, mask);
		/* Tick广播设备如果是基于高分辨率定时器模拟的则也不能算是本地的 */
		local = !(bc->features & CLOCK_EVT_FEAT_HRTIMER);
	}

        /* 仍然有其它CPU等着接收Tick广播 */
	if (!cpumask_empty(mask)) {
		/* 假设系统中所有CPU上的Tick设备中的定时事件设备的broadcast函数都是一样的 */
                /* 取第一个Tick设备 */
		td = &per_cpu(tick_cpu_device, cpumask_first(mask));
                /* 调用它的broadcast函数 */
		td->evtdev->broadcast(mask);
	}
	return local;
}

该函数首先检查需要广播服务的CPU位图中是否包含了本CPU,如果是的话会将其清除,然后函数退出的时候会返回真。接着,如果还有CPU等着接收Tick广播的话,就从所有这些CPU对应的Tick设备上挑选出一个,这里选的是第一个设备,然后调用它的广播函数。这里其实有一个假设,就是系统中所有CPU上的Tick设备中的定时事件设备的broadcast函数都被设置成一样的。在后面会看到它们确实都是一样的,被设置成了tick_broadcast函数。

3)切换到单次触发模式

在介绍高分辨率定时器层的时候提到过,在低精度模式下的周期处理函数hrtimer_run_queues中,每次都会调用tick_check_oneshot_change函数,判断目前是否可以切换到高精度模式。而在这个函数中,还会调用tick_is_oneshot_available函数判断Tick层是否已经准备好切换了:

int tick_is_oneshot_available(void)
{
        /* 获取代表当前CPU上定时事件设备的clock_event_device结构体 */
	struct clock_event_device *dev = __this_cpu_read(tick_cpu_device.evtdev);

        /* 如果当前CPU上没有定时事件设备或者不支持单次触发模式则返回0 */
	if (!dev || !(dev->features & CLOCK_EVT_FEAT_ONESHOT))
		return 0;
        /* 如果当前CPU上的定时事件设备也不支持C3_STOP模式则返回1 */
	if (!(dev->features & CLOCK_EVT_FEAT_C3STOP))
		return 1;
        /* 如果当前CPU上的定时事件设备支持C3_STOP模式则还要查看Tick广播层 */
	return tick_broadcast_oneshot_available();
}

该函数最后还会调用tick_broadcast_oneshot_available函数,看看Tick广播层是否已经准备好了:

bool tick_broadcast_oneshot_available(void)
{
	struct clock_event_device *bc = tick_broadcast_device.evtdev;

	return bc ? bc->features & CLOCK_EVT_FEAT_ONESHOT : false;
}

也就是说,即使当前CPU上有专有的定时事件设备,并且它是支持单次触发模式的,但是如果它还支持所谓的C3_STOP模式,并且当前Tick广播层的设备不支持单次触发模式,那么还是不能切换成高精度模式。

还是在介绍高分辨率定时器层的时候提到过,如果通过了测试正式准备切换到单次触发模式了,最终会调用tick_switch_to_oneshot函数。如果切换成功,函数的最后会调用tick_broadcast_switch_to_oneshot函数,将Tick广播层也切换到单次触发模式:

void tick_broadcast_switch_to_oneshot(void)
{
	struct clock_event_device *bc;
	unsigned long flags;

	raw_spin_lock_irqsave(&tick_broadcast_lock, flags);

	tick_broadcast_device.mode = TICKDEV_MODE_ONESHOT;
	bc = tick_broadcast_device.evtdev;
        /* 如果当前Tick广播设备不为空 */
	if (bc)
                /* 设置当前Tick广播设备到单次触发模式 */
		tick_broadcast_setup_oneshot(bc);

	raw_spin_unlock_irqrestore(&tick_broadcast_lock, flags);
}

所以,如果当前系统中用于Tick广播的定时事件设备不为0,则调用tick_broadcast_setup_oneshot将其设置到单次触发模式:

static void tick_broadcast_setup_oneshot(struct clock_event_device *bc)
{
	int cpu = smp_processor_id();

	if (!bc)
		return;

	/* 是否已经切换到单次触发模式了 */
	if (bc->event_handler != tick_handle_oneshot_broadcast) {
                /* 当前是否处于周期触发模式 */
		int was_periodic = clockevent_state_periodic(bc);

                /* 将定时事件设备的中断回调函数设置成tick_handle_oneshot_broadcast */
		bc->event_handler = tick_handle_oneshot_broadcast;

		/* 将tick_broadcast_mask拷贝到tmpmask */
		cpumask_copy(tmpmask, tick_broadcast_mask);
                /* 去掉当前的CPU */
		cpumask_clear_cpu(cpu, tmpmask);
                /* 合并需要提供周期Tick广播的CPU进入tick_broadcast_oneshot_mask中 */
		cpumask_or(tick_broadcast_oneshot_mask,
			   tick_broadcast_oneshot_mask, tmpmask);

                /* 如果当前处于周期触发模式并且还有CPU需要提供广播服务 */
		if (was_periodic && !cpumask_empty(tmpmask)) {
                        /* 将定时事件设备切换到单次触发模式 */
			clockevents_switch_state(bc, CLOCK_EVT_STATE_ONESHOT);
			tick_broadcast_init_next_event(tmpmask,
						       tick_next_period);
			tick_broadcast_set_event(bc, cpu, tick_next_period);
		} else
			bc->next_event = KTIME_MAX;
	} else {
		/* 去掉当前的CPU */
		tick_broadcast_clear_oneshot(cpu);
	}
}

如果系统中有多个CPU的话,要切换到高精度模式,应该每个CPU上都会有一个私有的支持单次触发模式的定时事件设备,每个CPU都会依次调用tick_switch_to_oneshot切换到高精度模式下,因此tick_broadcast_setup_oneshot函数会被调用多次。但是,对于Tick广播层来说,只有一个Tick广播设备,它是系统中所有CPU共用的。所以,只要系统中第一个CPU切换到高精度模式下了,则Tick广播设备就会被切换到单次触发模式了。但是,系统中的其它CPU的Tick设备还是处在周期触发模式下,还需要Tick广播层提供周期广播服务。因此,在计算tick_broadcast_oneshot_mask的时候还要考虑加入tick_broadcast_mask指定的CPU,并且对Tick广播设备进行编程,让它在下一次Tick到来的时间点触发。

另外,如果执行到了这个函数,说明本CPU已经不在空闲状态,并且当前CPU上的定时事件设备正要切换成单次触发状态肯定不会被停止,因此一定是不再需要Tick广播服务了。

static void tick_broadcast_init_next_event(struct cpumask *mask,
					   ktime_t expires)
{
	struct tick_device *td;
	int cpu;

        /* 按mask遍历所有Per CPU变量 */
	for_each_cpu(cpu, mask) {
		td = &per_cpu(tick_cpu_device, cpu);
		if (td->evtdev)
			td->evtdev->next_event = expires;
	}
}

可以看出来tick_broadcast_init_next_event函数实际上是遍历所有参数mask指定的CPU上的Tick设备,将其对应的代表定时事件设备结构体中的next_event变量设置成传递进来的expires参数的值。

static void tick_broadcast_set_event(struct clock_event_device *bc, int cpu,
				     ktime_t expires)
{
        /* 切换Tick广播设备到单次触发模式 */
	if (!clockevent_state_oneshot(bc))
		clockevents_switch_state(bc, CLOCK_EVT_STATE_ONESHOT);

        /* 对Tick广播设备进行编程让其在指定到期时间触发 */
	clockevents_program_event(bc, expires, 1);
        /* 设置Tick广播设备的亲缘性 */
	tick_broadcast_set_affinity(bc, cpumask_of(cpu));
}

tick_broadcast_set_event函数用来对Tick广播设备进行编程,让其在指定的到期时间上,在指定的CPU上触发中断。tick_broadcast_set_affinity函数用来设置Tick广播设备产生中断的CPU亲缘性:

static void tick_broadcast_set_affinity(struct clock_event_device *bc,
					const struct cpumask *cpumask)
{
        /* 定时事件设备是否支持设置CPU亲缘性 */
	if (!(bc->features & CLOCK_EVT_FEAT_DYNIRQ))
		return;

        /* CPU亲缘性是否没有更改 */
	if (cpumask_equal(bc->cpumask, cpumask))
		return;

	bc->cpumask = cpumask;
        /* 设置CPU亲缘性 */
	irq_set_affinity(bc->irq, bc->cpumask);
}

irq_set_affinity函数最终用来设置定时事件设备的CPU亲缘性,到期后触发指定CPU上的中断。

一旦切换到单次触发模式后,就不是像在周期触发模式那样,每次都是按照Tick周期触发了,而是按照需要触发。并且,Tick广播设备能切换到单次触发模式的前提是,系统中至少有一个CPU上的Tick设备被切换成了单次触发模式。由于多处理器系统基本上都是对称的(SMP),所以系统中剩下的CPU迟早也会注册同样的定时事件设备,最终各个CPU上的Tick设备都会切换到单次触发模式下。

4)单次触发模式事件处理

前面分析也提到了,在单次触发模式下,被Tick广播层选中的定时事件设备的中断回调函数会被设置成tick_handle_oneshot_broadcast:

static void tick_handle_oneshot_broadcast(struct clock_event_device *dev)
{
	struct tick_device *td;
	ktime_t now, next_event;
	int cpu, next_cpu = 0;
	bool bc_local;

	raw_spin_lock(&tick_broadcast_lock);
	dev->next_event = KTIME_MAX;
	next_event = KTIME_MAX;
	cpumask_clear(tmpmask);
	now = ktime_get();
	/* 遍历所有需要Tick广播服务的CPU */
	for_each_cpu(cpu, tick_broadcast_oneshot_mask) {
		/* 如果系统中没有CPU需要Tick广播服务则直接退出 */
		if (!IS_ENABLED(CONFIG_SMP) &&
		    cpumask_empty(tick_broadcast_oneshot_mask))
			break;

		td = &per_cpu(tick_cpu_device, cpu);
		if (td->evtdev->next_event <= now) {
                        /* 如果对应CPU的Tick设备已经到期 */
			cpumask_set_cpu(cpu, tmpmask);
			/* 设置tick_broadcast_pending_mask对应CPU的位 */
			cpumask_set_cpu(cpu, tick_broadcast_pending_mask);
		} else if (td->evtdev->next_event < next_event) {
                        /* 找出最近Tick设备即将要到期的那个CPU和到期时间 */
			next_event = td->evtdev->next_event;
			next_cpu = cpu;
		}
	}

	/* 将本CPU从tick_broadcast_pending_mask中清除 */
	cpumask_clear_cpu(smp_processor_id(), tick_broadcast_pending_mask);

	/* 并入所有tick_broadcast_force_mask全局变量中指定的CPU */
	cpumask_or(tmpmask, tmpmask, tick_broadcast_force_mask);
        /* 清空tick_broadcast_force_mask全局变量 */
	cpumask_clear(tick_broadcast_force_mask);

	/* 排除掉已经不在线的CPU */
	if (WARN_ON_ONCE(!cpumask_subset(tmpmask, cpu_online_mask)))
		cpumask_and(tmpmask, tmpmask, cpu_online_mask);

	/* 发送Tick广播 */
	bc_local = tick_do_broadcast(tmpmask);

	/* 对广播设备进行编程,让它在下一个到期时间在对应的CPU上触发中断。 */
	if (next_event != KTIME_MAX)
		tick_broadcast_set_event(dev, next_cpu, next_event);

	raw_spin_unlock(&tick_broadcast_lock);

        /* 如果本CPU也需要Tick广播服务 */
	if (bc_local) {
		td = this_cpu_ptr(&tick_cpu_device);
                /* 直接调用当前CPU上的Tick设备的事件处理函数 */
		td->evtdev->event_handler(td->evtdev);
	}
}

Tick广播层工作在周期触发模式和单次触发模式有很大的不同。周期触发模式没有所谓的CPU亲缘性的概念,Tick广播层会按照一个Tick周期触发一个中断,系统中的任何CPU都有可能接收到这个中断并对其进行处理。而单次触发模式具有CPU亲缘性的概念,如果作为Tick广播设备的定时事件设备支持的话,会触发指定CPU上的中断,将其激活,处理到期的定时器。

切换到单次触发模式后,将检查tick_broadcast_oneshot_mask全局变量来确定哪些CPU需要Tick广播服务,而不再是tick_broadcast_mask全局变量。

5)打开或关闭对本CPU的Tick广播服务

在cpuidle驱动注册的时候(__cpuidle_register_driver函数内,代码位于drivers/cpuidle/drivers.c。),如果某些状态包含CPUIDLE_FLAG_TIMER_STOP选项,则会调用tick_broadcast_enable函数,打开对本CPU的Tick广播服务;而在驱动卸载的时候(__cpuidle_unregister_driver函数内),会相应调用tick_broadcast_disable函数,关闭对本CPU的Tick广播服务。

static inline void tick_broadcast_enable(void)
{
	tick_broadcast_control(TICK_BROADCAST_ON);
}
static inline void tick_broadcast_disable(void)
{
	tick_broadcast_control(TICK_BROADCAST_OFF);
}

最终都是调用的tick_broadcast_control函数,只不过传入的参数不一样:

void tick_broadcast_control(enum tick_broadcast_mode mode)
{
	struct clock_event_device *bc, *dev;
	struct tick_device *td;
	int cpu, bc_stopped;
	unsigned long flags;

	/* 获得tick_broadcast_lock自旋锁并关中断 */
	raw_spin_lock_irqsave(&tick_broadcast_lock, flags);
	td = this_cpu_ptr(&tick_cpu_device);
	dev = td->evtdev;

	/* 如果当前CPU上的Tick设备不支持C3_STOP模式则直接退出 */
	if (!dev || !(dev->features & CLOCK_EVT_FEAT_C3STOP))
		goto out;

        /* 如果当前CPU上的Tick设备不可以正常工作则直接退出 */
	if (!tick_device_is_functional(dev))
		goto out;

	cpu = smp_processor_id();
	bc = tick_broadcast_device.evtdev;
        /* 当前Tick广播设备是否是停止的 */
	bc_stopped = cpumask_empty(tick_broadcast_mask);

	switch (mode) {
	case TICK_BROADCAST_FORCE:
                /* 强制打开 */
		tick_broadcast_forced = 1;
	case TICK_BROADCAST_ON:
                /* 设置tick_broadcast_on中当前CPU对应的位 */
		cpumask_set_cpu(cpu, tick_broadcast_on);
                /* 设置tick_broadcast_mask中当前CPU对应的位 */
		if (!cpumask_test_and_set_cpu(cpu, tick_broadcast_mask)) {
                        /* 如果之前没有由Tick广播层对当前CPU发送Tick广播 */
			/* 如果Tick广播设备存在且不是由高分辨率定时器模拟的且处在周期触发模式 */
			if (bc && !(bc->features & CLOCK_EVT_FEAT_HRTIMER) &&
			    tick_broadcast_device.mode == TICKDEV_MODE_PERIODIC)
                                /* 关闭本CPU的Tick设备 */
				clockevents_shutdown(dev);
		}
		break;

	case TICK_BROADCAST_OFF:
                /* 如果强制打开不允许关闭 */
		if (tick_broadcast_forced)
			break;
                /* 清除tick_broadcast_on中当前CPU对应的位 */
		cpumask_clear_cpu(cpu, tick_broadcast_on);
                /* 清除tick_broadcast_mask中当前CPU对应的位 */
		if (cpumask_test_and_clear_cpu(cpu, tick_broadcast_mask)) {
                        /* 如果之前已经由Tick广播层对当前CPU发送Tick广播 */
                        /* 如果当前Tick广播层还处在周期触发模式 */
			if (tick_broadcast_device.mode ==
			    TICKDEV_MODE_PERIODIC)
                                /* 打开本CPU的Tick设备生成Tick */
				tick_setup_periodic(dev, 0);
		}
		break;
	}

	if (bc) {
		if (cpumask_empty(tick_broadcast_mask)) {
                        /* 如果没有CPU需要提供Tick广播服务 */
                        /* 如果当前Tick广播设备是打开的 */
			if (!bc_stopped)
                                /* 关闭Tick广播设备 */
				clockevents_shutdown(bc);
		} else if (bc_stopped) {
                        /* 如果有CPU需要提供Tick广播服务且当前Tick广播设备是关闭的 */
                        /* 按照当前Tick广播层所处的模式分别打开Tick广播设备 */
			if (tick_broadcast_device.mode == TICKDEV_MODE_PERIODIC)
				tick_broadcast_start_periodic(bc);
			else
				tick_broadcast_setup_oneshot(bc);
		}
	}
out:
        /* 释放tick_broadcast_lock自旋锁并开中断 */
	raw_spin_unlock_irqrestore(&tick_broadcast_lock, flags);
}
EXPORT_SYMBOL_GPL(tick_broadcast_control);

cpuidle驱动的注册时机应该是在系统所有的定时事件设备都已经注册完成之后,也就是在调用tick_broadcast_control函数之后不会有新的定时事件设备再注册进系统中。因此,如果当前CPU上的Tick定时事件设备不可用或者不工作,那么没得选,肯定要靠Tick广播层提供服务,因此也没必要设置tick_broadcast_on打开或关闭了。后面会介绍(在tick_device_uses_broadcast函数中),在这种情况下,其实在这个不工作的定时事件设备被当前CPU选中作为产生Tick的设备时,就已经通知Tick广播层对该CPU打开了Tick广播服务。而如果当前CPU上的Tick设备不支持C3_STOP状态,那它自己就可以搞定当前CPU上的Tick,肯定不需要Tick广播服务了。

当Tick广播层工作在周期触发模式的时候,只要CPU有可能被切换到会关闭本地定时事件设备的状态,且本地定时事件设备支持C3_STOP状态时,就会让Tick广播层提供服务了,即使当前CPU还没进入那种状态。

6)Tick广播服务注册

不是任何时候对任何CPU都需要启动Tick广播服务的,前面介绍了在cpuidle驱动中主动打开或关闭某个CPU的Tick广播服务的情况。还有一种情况,如果当前CPU上的Tick设备将要发生改变的时候也可能对Tick广播服务产生影响。

在介绍Tick层的时候,曾经提到过,当有一个新的定时事件设备注册上来,需要替换当前CPU上Tick层的设备时,会调用tick_setup_device函数,在这个函数中,会调用tick_device_uses_broadcast函数:

int tick_device_uses_broadcast(struct clock_event_device *dev, int cpu)
{
	struct clock_event_device *bc = tick_broadcast_device.evtdev;
	unsigned long flags;
	int ret = 0;

	raw_spin_lock_irqsave(&tick_broadcast_lock, flags);

	/* 定时事件设备是否是“假”的 */
	if (!tick_device_is_functional(dev)) {
                /* 将定时事件设备的中断回调函数设置成tick_handle_periodic */
		dev->event_handler = tick_handle_periodic;
                /* 设置定时事件设备的broadcast函数 */
		tick_device_setup_broadcast_func(dev);
                /* 该CPU需要提供Tick周期广播服务 */
		cpumask_set_cpu(cpu, tick_broadcast_mask);
                /* 根据当前Tick广播层所处的模式启动Tick广播服务 */
		if (tick_broadcast_device.mode == TICKDEV_MODE_PERIODIC)
			tick_broadcast_start_periodic(bc);
		else
			tick_broadcast_setup_oneshot(bc);
		ret = 1;
	} else {
		/* 如果定时事件设备不支持C3_STOP模式 */
		if (!(dev->features & CLOCK_EVT_FEAT_C3STOP))
                        /* 不需要对这个CPU提供Tick广播服务 */
			cpumask_clear_cpu(cpu, tick_broadcast_mask);
		else
                        /* 否则设置定时事件设备的broadcast函数 */
			tick_device_setup_broadcast_func(dev);

		/* 如果对该CPU的Tick广播服务被关闭了 */
		if (!cpumask_test_cpu(cpu, tick_broadcast_on))
                        /* 清除对该CPU的Tick广播服务 */
			cpumask_clear_cpu(cpu, tick_broadcast_mask);

		switch (tick_broadcast_device.mode) {
		case TICKDEV_MODE_ONESHOT:
                        /* 在单次触发模式下 */
			/* 直接清除对该CPU的Tick单次广播服务 */
			tick_broadcast_clear_oneshot(cpu);
			ret = 0;
			break;

		case TICKDEV_MODE_PERIODIC:
                        /* 在周期触发模式下 */
			/* 如果系统中没有一个CPU需要提供Tick周期广播服务 */
			if (cpumask_empty(tick_broadcast_mask) && bc)
                                /* 关闭Tick广播设备 */
				clockevents_shutdown(bc);
			/* 如果Tick广播设备存在且不是由高分辨率定时器模拟的 */
			if (bc && !(bc->features & CLOCK_EVT_FEAT_HRTIMER))
                                /* 如果该CPU需要Tick周期广播服务 */
				ret = cpumask_test_cpu(cpu, tick_broadcast_mask);
			break;
		default:
			break;
		}
	}
	raw_spin_unlock_irqrestore(&tick_broadcast_lock, flags);
	return ret;
}

tick_device_uses_broadcast函数主要是检查当前CPU上的Tick定时事件设备的特性,并确定是否要启用对这个CPU的Tick广播服务。如果这个函数返回非0,则表示当前CPU上的Tick将完全有Tick广播层来负责。

如果当前CPU打算选择一个“假”的定时事件设备作为Tick设备,那么没有办法,只能由Tick广播层对其提供Tick广播服务了。并且这个结果是不受tick_broadcast_on变量控制的,也就是即使已经显式关闭了对本CPU的Tick广播服务,照样还需要Tick广播层提供服务,这也很好理解,毕竟自己CPU的Tick设备已经没法工作了,必须要寻求外部的帮助。

如果当前CPU选择的定时事件设备是真的,且不支持C3_STOP模式,那么在这个CPU上的Tick完全可以由这个本地设备搞定,就没必要Tick广播层对其提供服务了,因此可以直接清除。还有,如果对该CPU的Tick周期广播服务被关闭了,也需要清除对该CPU的Tick广播服务。如果当前Tick广播层已经工作在单次触发模式了,那非常简单,直接清除对该CPU的单次Tick广播服务就行了,因为执行到这个函数就证明当前CPU不是在空闲状态,对应CPU上的定时事件设备不会被停止掉。但是,如果当前Tick广播层还是工作在周期模式,情况就有点复杂了。如果没有任何设备需要提供Tick周期广播服务,则当前的Tick广播设备就可以关闭了。并且,如果当前的Tick广播设备存在,且不是有高分辨率定时器模拟的,且当前CPU还需要提供Tick广播服务,则返回非0,也就是当前CPU上的周期Tick将完全由Tick广播层来提供。

函数tick_device_setup_broadcast_func负责设置指定定时事件设备的broadcast函数:

static void tick_device_setup_broadcast_func(struct clock_event_device *dev)
{
	if (!dev->broadcast)
		dev->broadcast = tick_broadcast;
	if (!dev->broadcast) {
		pr_warn_once("%s depends on broadcast, but no broadcast function available\n",
			     dev->name);
		dev->broadcast = err_broadcast;
	}
}

可以看到,如果不出问题的话,每个CPU上私有的定时事件设备的广播函数都被设置成了tick_broadcast。

7)打开或关闭本地定时器

当系统中的某个CPU要进入或退出一个标记有CPUIDLE_FLAG_TIMER_STOP选项的空闲状态的时候,会调用tick_broadcast_enter函数,从而告诉Tick广播层属于本CPU的本地定时事件设备就要停止掉了,需要Tick广播层提供服务。相反,如果退出了空闲状态之后,会调用tick_broadcast_exit函数,恢复本CPU的定时事件设备,停止掉针对本CPU的Tick广播服务(代码位于include/linux/tick.h中):

static inline int tick_broadcast_enter(void)
{
	return tick_broadcast_oneshot_control(TICK_BROADCAST_ENTER);
}
static inline void tick_broadcast_exit(void)
{
	tick_broadcast_oneshot_control(TICK_BROADCAST_EXIT);
}

就是直接调用了tick_broadcast_oneshot_control函数,传递了不同的参数(代码位于kernel/time/tick-common.c中):

int tick_broadcast_oneshot_control(enum tick_broadcast_state state)
{
	struct tick_device *td = this_cpu_ptr(&tick_cpu_device);

        /* 如果本CPU的Tick设备不支持C3_STOP状态则直接退出 */
	if (!(td->evtdev->features & CLOCK_EVT_FEAT_C3STOP))
		return 0;

	return __tick_broadcast_oneshot_control(state);
}
EXPORT_SYMBOL_GPL(tick_broadcast_oneshot_control);

如果本CPU的Tick设备不支持C3_STOP状态,就意味着它不会被停掉,那当然就不用做什么特殊操作了,直接返回就可以了。否则,会接着调用__tick_broadcast_oneshot_control函数:

int __tick_broadcast_oneshot_control(enum tick_broadcast_state state)
{
	struct clock_event_device *bc, *dev;
	int cpu, ret = 0;
	ktime_t now;

	/* 如果Tick广播设备不存在则直接返回-EBUSY */
	if (!tick_broadcast_device.evtdev)
		return -EBUSY;

	dev = this_cpu_ptr(&tick_cpu_device)->evtdev;

	raw_spin_lock(&tick_broadcast_lock);
	bc = tick_broadcast_device.evtdev;
	cpu = smp_processor_id();

	if (state == TICK_BROADCAST_ENTER) {
                /* 要进入空闲状态 */
		/* 判断当前CPU是否能进入休眠状态 */
		ret = broadcast_needs_cpu(bc, cpu);
		if (ret)
			goto out;

		/* 如果Tick广播层还处在周期触发模式 */
		if (tick_broadcast_device.mode == TICKDEV_MODE_PERIODIC) {
			/* 如果Tick广播设备是由高分辨率定时器模拟的则返回-EBUSY */
			if (bc->features & CLOCK_EVT_FEAT_HRTIMER)
				ret = -EBUSY;
                        /* 否则返回0 */
			goto out;
		}

                /* 设置tick_broadcast_oneshot_mask中当前CPU对应的位 */
		if (!cpumask_test_and_set_cpu(cpu, tick_broadcast_oneshot_mask)) {
                        /* 如果之前没有由Tick广播层为当前CPU服务 */
			WARN_ON_ONCE(cpumask_test_cpu(cpu, tick_broadcast_pending_mask));

			/* 尝试关闭本CPU上的定时事件设备 */
			broadcast_shutdown_local(bc, dev);

			/* 如果tick_broadcast_force_mask中对应当前CPU的位被设置了 */
			if (cpumask_test_cpu(cpu, tick_broadcast_force_mask)) {
                                /* 返回-EBUSY先不让休眠 */
				ret = -EBUSY;
			} else if (dev->next_event < bc->next_event) {
                                /* 如果当前要休眠CPU上的Tick设备到期时间早于Tick广播设备到期时间 */
                                /* 用当前CPU上Tick设备的到期时间对Tick广播设备重编程 */
				tick_broadcast_set_event(bc, cpu, dev->next_event);
				/* 再次判断当前CPU是否能进入休眠状态 */
				ret = broadcast_needs_cpu(bc, cpu);
				if (ret) {
                                        /* 如果不能清除tick_broadcast_oneshot_mask中当前CPU对应的位 */
					cpumask_clear_cpu(cpu,
						tick_broadcast_oneshot_mask);
				}
			}
		}
	} else {
                /* 要退出空闲状态 */
		if (cpumask_test_and_clear_cpu(cpu, tick_broadcast_oneshot_mask)) {
                        /* 打开当前CPU的定时事件设备并切换到单次触发模式 */
			clockevents_switch_state(dev, CLOCK_EVT_STATE_ONESHOT);
			/* 清除tick_broadcast_pending_mask中当前CPU对应的位 */
			if (cpumask_test_and_clear_cpu(cpu,
				       tick_broadcast_pending_mask))
                                /* 如果清除之前tick_broadcast_pending_mask中当前CPU对应的位已经被设置则直接退出 */
				goto out;

			/* 如果当前CPU上Tick设备的到期时间是KTIME_MAX则直接退出 */
			if (dev->next_event == KTIME_MAX)
				goto out;
			/* 获得当前时间 */
			now = ktime_get();
                        /* 如果当前CPU上的Tick设备已经到期 */
			if (dev->next_event <= now) {
				cpumask_set_cpu(cpu, tick_broadcast_force_mask);
				goto out;
			}
			/* 对当前CPU上的Tick设备进行编程 */
			tick_program_event(dev->next_event, 1);
		}
	}
out:
	raw_spin_unlock(&tick_broadcast_lock);
	return ret;
}

tick_broadcast_pending_mask是一个全局变量,是在单次触发模式事件处理函数tick_handle_oneshot_broadcast里面设置的。在遍历所有需要提供Tick广播服务的CPU时,如果发现其定时事件设备已经到期了,就会设置tick_broadcast_pending_mask变量相应的位。这个变量主要用来处理这样一种特殊的竞态情况,所有到期的Tick设备,最终在tick_handle_oneshot_broadcast函数里都会调用广播函数对其发送夸处理器中断(IPI)将其唤醒,但是这个时候如果要被唤醒的CPU突然被其它条件唤醒了后,会退出空闲状态,如果它不知道可能后面会被Tick广播层唤醒的话,就会立即执行到期处理函数,而后面到来的夸处理器中断又会再让它执行一次到期处理函数,就会把时间搞乱。所以,与其这样还不如不做任何处理,等着下面的夸处理器中断到来反正还要处理一次。因此,在当前CPU要退出空闲状态时,如果发现了对应tick_broadcast_pending_mask上的位被设置了,就可以直接退出了,不需要再对当前CPU上的Tick设备进行编程。

tick_broadcast_force_mask也是一个全局变量,不过处理的情况跟前面刚好相反。这次是某个空闲的CPU先被其它条件唤醒了,它发现当前CPU上的Tick设备已经到期了,如果用当前时间对本CPU的Tick设备编程显然不划算,还不如等Tick广播设备的夸处理器中断来处理。因为,本CPU的Tick设备到期了,也就意味着Tick广播设备马上也要到期了。因此,在当前CPU进入空闲状态时,如果检查发现对应tick_broadcast_force_mask上的位被设置了,就知道马上自己又要被激活了,如果现在进入休眠不划算,因此需要返回-EBUSY。

如果Tick广播层还处在周期触发模式,且当前的Tick广播设备是由一个高分辨率定时器模拟的,那么当尝试进入空闲状态时,都是返回-EBUSY,强制不让进入,保证当前CPU上的Tick还是由自己负责。

broadcast_needs_cpu函数用来判断当前的Tick广播设备是否是一个用高分辨率定时器模拟的设备,并且这个设备是否是绑定在参数cpu指定的CPU上的:

static int broadcast_needs_cpu(struct clock_event_device *bc, int cpu)
{
        /* Tick广播设备是否是用高分辨率定时器模拟的 */
	if (!(bc->features & CLOCK_EVT_FEAT_HRTIMER))
		return 0;
        /* 到期时间是KTIME_MAX意味着该设备可以被停止 */
	if (bc->next_event == KTIME_MAX)
		return 0;
        /* 模拟Tick广播设备的高分辨率定时器是否绑定到指定的CPU上 */
	return bc->bound_on == cpu ? -EBUSY : 0;
}

如果条件都成立的话,仍然需要当前CPU为系统中的其它处理器提供广播服务,所以该函数将返回-EBUSY,表示本CPU忙,不能切换到空闲状态。

broadcast_shutdown_local函数会根据Tick广播设备和本CPU的定时事件设备的状态,看看是否需要关闭本CPU的定时事件设备:

static void broadcast_shutdown_local(struct clock_event_device *bc,
				     struct clock_event_device *dev)
{
	/* 如果Tick广播设备是由高分辨率定时器模拟的 */
	if (bc->features & CLOCK_EVT_FEAT_HRTIMER) {
                /* 是否还需要当前CPU继续工作 */
		if (broadcast_needs_cpu(bc, smp_processor_id()))
			return;
                /* 当前CPU上定时事件设备的到期时间是否小于Tick广播设备的到期时间 */
		if (dev->next_event < bc->next_event)
			return;
	}
	clockevents_switch_state(dev, CLOCK_EVT_STATE_SHUTDOWN);
}

如果Tick广播设备不是高分辨率定时器模拟的话,很简单直接关闭就好了。如果是的话,在两种情况下不能关闭。一是,如果这个模拟Tick广播的高分辨率定时器刚好运行在当前CPU上时,理由和前面一样;二是,如果当前CPU上定时事件设备的到期时间早于Tick广播设备的到期时间。第一种情况很好理解,那第二种情况又是为什么呢?这是因为,如果当前CPU上定时事件设备的到期时间是否小于Tick广播设备的到期时间,那么在__tick_broadcast_oneshot_control函数后面会调用tick_broadcast_set_event函数,用这个时间对Tick广播设备进行重编程,但这个重编程的设备又是通过高分辨率定时器模拟的,这样就会将该高分辨率定时器迁移到当前CPU上,如果之前把它又关了,那就没法再触发Tick广播了。

和工作在周期触发模式不同,当Tick广播层工作在单次触发模式的时候,要等到真的当前CPU进入到会关闭本地定时事件设备的状态时,才会由Tick广播层提供服务。

8)广播的发送和接收

在前面提到过,系统中所有CPU上的Tick设备的broadcast函数都会被设置成tick_broadcast,这个函数是分平台实现的,对于Arm64处理器来说,其实现如下(代码位于arch/arm64/kernel/smp.c中):

#ifdef CONFIG_GENERIC_CLOCKEVENTS_BROADCAST
void tick_broadcast(const struct cpumask *mask)
{
	smp_cross_call(mask, IPI_TIMER);
}
#endif

可以看到,就是将IPI_TIMER的夸处理器中断发送给参数mask指定的CPU。

而当“远端”的CPU收到了本CPU发送的夸处理器中断后,会调用handle_IPI函数进行处理:

void handle_IPI(int ipinr, struct pt_regs *regs)
{
	unsigned int cpu = smp_processor_id();
	struct pt_regs *old_regs = set_irq_regs(regs);

	if ((unsigned)ipinr < NR_IPI) {
		trace_ipi_entry_rcuidle(ipi_types[ipinr]);
		__inc_irq_stat(cpu, ipi_irqs[ipinr]);
	}

	switch (ipinr) {
	......
#ifdef CONFIG_GENERIC_CLOCKEVENTS_BROADCAST
	case IPI_TIMER:
		irq_enter();
		tick_receive_broadcast();
		irq_exit();
		break;
#endif
    ......
	default:
		pr_crit("CPU%u: Unknown IPI message 0x%x\n", cpu, ipinr);
		break;
	}

	if ((unsigned)ipinr < NR_IPI)
		trace_ipi_exit_rcuidle(ipi_types[ipinr]);
	set_irq_regs(old_regs);
}

可以看到,对于IPI_TIMER夸处理器中断,其处理函数是tick_receive_broadcast:

#ifdef CONFIG_GENERIC_CLOCKEVENTS_BROADCAST
int tick_receive_broadcast(void)
{
	struct tick_device *td = this_cpu_ptr(&tick_cpu_device);
	struct clock_event_device *evt = td->evtdev;

	if (!evt)
		return -ENODEV;

	if (!evt->event_handler)
		return -EINVAL;

	evt->event_handler(evt);
	return 0;
}
#endif

这下就简单了,直接找到当前CPU上的定时事件设备,然后直接调用它的中断处理函数就可以了,仿佛就是由这个定时事件设备自己触发的定时中断一样。需要注意的是,收到了夸处理器中断的请求后,并不会打开当前CPU上的定时事件设备。

9)基于高分辨率定时器的Tick广播设备

前面提到过,如果Tick层要切换到单次触发模式,或者高分辨率定时器层要切换到高精度模式,对于定时事件设备来说,必须要满足下面两个条件:

  1. 本地CPU的定时事件设备必须支持单次触发模式;
  2. 如果本地CPU的定时事件设备支持C3_STOP模式,那么还必须要求Tick广播设备支持单次触发模式。

但是,如果本地CPU是支持单次触发模式的,且为了节电的目的也支持C3_STOP模式,不过全系统中没有任何共享的定时事件设备支持单次触发模式怎么办?永远也切换不到更高级别的模式上去了。Linux内核想到了一个办法,让系统中的某一个CPU不进入休眠模式,它的定时事件设备保持打开的状态,让它为系统中的其它CPU服务,去唤醒其它休眠的CPU。具体的操作方式是系统设计了一个用来模拟Tick广播设备的高分辨率定时器,其初始化代码如下(代码位于kernel/time/tick-broadcast-hrtimer.c中):

void tick_setup_hrtimer_broadcast(void)
{
        /* 初始化该高分辨率定时器 */
	hrtimer_init(&bctimer, CLOCK_MONOTONIC, HRTIMER_MODE_ABS_HARD);
        /* 将该高分辨率定时器的处理函数设置成bc_handler */
	bctimer.function = bc_handler;
        /* 注册模拟的定时事件设备 */
	clockevents_register_device(&ce_broadcast_hrtimer);
}

bctimer是一个全局变量,表示模拟Tick广播的那个高分辨率定时器:

static struct hrtimer bctimer;

这个高分辨率定时器的处理函数被设置成了bc_handler:

static enum hrtimer_restart bc_handler(struct hrtimer *t)
{
	ce_broadcast_hrtimer.event_handler(&ce_broadcast_hrtimer);

	return HRTIMER_NORESTART;
}

所以,定时器到期了之后会驱动模拟出来的一个定时事件设备ce_broadcast_hrtimer,定义如下:

static struct clock_event_device ce_broadcast_hrtimer = {
	.name			= "bc_hrtimer",
	.set_state_shutdown	= bc_shutdown,
	.set_next_ktime		= bc_set_next,
	.features		= CLOCK_EVT_FEAT_ONESHOT |
				  CLOCK_EVT_FEAT_KTIME |
				  CLOCK_EVT_FEAT_HRTIMER,
	.rating			= 0,
	.bound_on		= -1,
	.min_delta_ns		= 1,
	.max_delta_ns		= KTIME_MAX,
	.min_delta_ticks	= 1,
	.max_delta_ticks	= ULONG_MAX,
	.mult			= 1,
	.shift			= 0,
	.cpumask		= cpu_possible_mask,
};

这个通过高分辨率定时器模拟出来的定时事件设备只支持单次触发模式,不支持周期触发模式。CLOCK_EVT_FEAT_HRTIMER唯一表示这个定时事件设备是用高分辨率定时器模拟的,不是真的物理设备,可以通过判断这个特征对其进行特殊处理。它的cpumask是系统中所有存在的CPU,不是和某个CPU绑定的,因此在注册的时候不会被当前CPU给截获掉,而是只能用来作为Tick广播设备使用。该设备的触发时间设置函数被设置成了bc_set_next:

static int bc_set_next(ktime_t expires, struct clock_event_device *bc)
{
	RCU_NONIDLE( {
                /* 激活用来模拟Tick广播设备的高分辨率定时器 */
		hrtimer_start(&bctimer, expires, HRTIMER_MODE_ABS_PINNED_HARD);
		/* 更新绑定的CPU */
		bc->bound_on = bctimer.base->cpu_base->cpu;
	} );
	return 0;
}

注意,这个挑选出来不休眠的CPU不是一直不变的,由于激活高分辨率定时器的时候有可能会发生定时器的迁移,系统中任何一个CPU都可能被挑选出来,但每次系统中只会有一个CPU不能休眠。由于会发生迁移,因此在激活之后要更新设备的bound_on参数。

如果当前Tick广播设备是高分辨率定时器模拟的,且Tick广播层处于周期触发模式,那么各个CPU上的Tick都还是由自己负责,哪怕其支持C3_STOP模式,自己的定时事件设备永远不会被停掉,CPU也不会彻底休眠。

你可能感兴趣的:(Arm64,Linux,ARM,Linux,时间子系统,Tick广播)