在Linux早期多核支持阶段,存在一种主核(boot CPU)通过定时器中断来通知从核(其他CPU)的方式。这种方式主要用于多核间的通信和任务调度。
采用的方法之一是使用IPI(中断处理器间中断)进行核间通信。主核通过定时器中断来触发IPI,从而通知从核进行相应的处理。
其基本流程如下:
主核初始化一个定时器,设置定时器中断处理函数。
在定时器中断处理函数中,主核向从核发送IPI。
从核收到IPI后,执行相应的中断处理函数。

IPI(处理器间中断)
// 主核发送IPI
void arch_send_call_function_ipi_mask(const struct cpumask *mask)
{
send_IPI_mask(mask, CALL_FUNCTION_VECTOR);
}
// 从核处理IPI
__visible void __irq_entry smp_rescheduling_interrupt(struct pt_regs *regs)
{
ack_APIC_irq();
scheduler_ipi();
}
这种方式在早期 SMP 系统中,由于硬件限制和设计简化,可能只有一个全局的定时器,由主核(通常是 CPU0)负责处理,然后通过 IPI 通知其他从核。这种方式存在以下问题:
- 可扩展性差:随着 CPU 数量增加,主核的负担加重。
- 延迟高:从核的时钟中断依赖于主核的定时器周期和 IPI 传递。
- 功耗问题:从核在空闲时可能仍然需要响应 IPI,无法进入深度的休眠状态。
2、完全去中心化架构
现代 Linux 内核采用了高度分布式的每 CPU 定时器架构,每个 CPU 都有独立的定时器管理和调度能力。这种设计显著提高了多核系统的性能和可扩展性。
在系统启动时,每个 CPU 都会初始化自己的时钟事件设备。Linux 内核将定时器抽象为时钟事件设备,用 struct clock_event_device 表示。每个 CPU 都有自己的时钟事件设备,它负责在指定时间产生中断。这个结构体的定义深刻体现了 C/C++ 在系统编程中的关键作用。
struct clock_event_device {
void (*event_handler)(struct clock_event_device *);
int (*set_next_event)(unsigned long evt, struct clock_event_device *);
/* ... 其他字段 ... */
unsigned long mult;
unsigned long shift;
u64 min_delta_ns;
u64 max_delta_ns;
const char *name;
int rating;
int irq;
cpumask_t cpumask;
struct list_head list;
} ____cacheline_aligned;
每个 CPU 的时钟中断处理函数通常是 tick_handle_periodic(对于周期性模式)或 tick_handle_oneshot(对于单次触发模式)。在中断处理函数中,会调用 update_process_times 来更新进程时间、处理定时器软中断等。
// 周期性时钟中断处理函数
void tick_handle_periodic(struct clock_event_device *dev)
{
// 获取当前CPU的编号
int cpu = smp_processor_id();
// 更新jiffies(全局时间)并处理时间更新
tick_periodic(cpu);
// 如果该时钟事件设备是周期性的,则重新设置下一次中断
if (dev->mode == CLOCK_EVT_MODE_PERIODIC)
dev->set_next_event(dev->next_event, dev);
}
// 在tick_periodic中,会调用update_process_times
void update_process_times(int user_tick)
{
struct task_struct *p = current;
// 更新进程运行时间
account_process_tick(p, user_tick);
// 触发定时器软中断
run_local_timers();
rcu_check_callbacks(cpu, user_tick);
// 调度器tick
scheduler_tick();
// 计算负载等
calc_global_load();
}
每个 CPU 通过自己的定时器中断触发调度器 tick,从而进行进程调度、负载均衡等操作。
void scheduler_tick(void)
{
int cpu = smp_processor_id();
struct rq *rq = cpu_rq(cpu);
struct task_struct *curr = rq->curr;
// 更新运行时间
update_rq_clock(rq);
curr->sched_class->task_tick(rq, curr, 0);
// 触发负载均衡,但通常不是每个tick都进行,而是间隔进行
trigger_load_balance(rq);
}
现代 Linux 内核为每个 CPU 配备独立的定时器,带来了诸多好处:每个 CPU 独立处理自己的定时中断,不会随着 CPU 数量增加而产生瓶颈;每个 CPU 可以独立设置定时器,无需等待主核的通知,响应更及时。
3、核间一致性
在 Linux 中,尽管每个核心都有自己的定时器硬件和定时器中断,但系统时间(也称为墙上时间)是全局的,由内核维护一个统一的系统时间。这个系统时间通常基于一个全局的时钟源(例如时钟芯片,如 HPET、ACPI PM Timer 等)来更新。
然而,在多核系统中,每个核心可能会独立读取本地时钟源(如 TSC,时间戳计数器)来获取当前时间,但由于每个核心的 TSC 可能不同步,因此内核需要做一些工作来保证时间的一致性。
其实现方式可以概括为以下几个关键点:
- 全局时钟源初始化:在系统启动时,会选择一个全局的时钟源,并初始化一个全局的时钟事件设备(可能由某个核心负责,或者使用广播机制)。
- 每 CPU 设备注册:每个核心在启动时,会初始化自己的时钟事件设备,并将其注册到时钟事件框架中。
- 时间同步机制:系统时间通常由全局时钟源和时钟事件设备来更新。在早期的内核中,可能由一个核心(如引导核心)负责更新系统时间,而其他核心则通过读取全局变量来获取系统时间。但是,这样可能会导致其他核心读取到过时的系统时间,因此需要采用一些同步机制。
在现代 Linux 内核中,使用了更复杂的方法来保证多核间时间的同步。例如,每个核心在读取时间时,会综合考虑全局时钟源和本地的偏移。内核会维护一个每 CPU 变量,记录该 CPU 与全局系统时间之间的偏移(或者记录每个 CPU 的 TSC 与全局时间的对应关系)。当某个核心需要获取当前时间时,它会读取本地 TSC,然后通过查询这个每 CPU 的偏移映射,计算出准确的全局系统时间,从而保证所有核心看到的时间是一致的。
这种“去中心化中断 + 中心化时间同步”的架构,是现代多核 Linux 系统能够高效、准确运行的关键基础之一。想了解更多系统底层的技术原理与实现,欢迎到 云栈社区 交流探讨。