文章目录
1. 前言
限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。
2. 背景
本文以 ARMv7
架构 + GIC v2
中断芯片 + Linux 4.14
为背景,分析 IRQ 中断 的 CPU 亲和性 (Affinity)
设置过程。
3. 什么是中断的 CPU 亲和性
什么是 中断的 CPU 亲和性
?是指接收处理某个中断信号的 CPU 集合
。
3. IRQ 中断 默认的 CPU 亲和性
Linux 下的 IRQ 中断,有一个默认的亲和性
设置:
start_kernel()
early_irq_init()
init_irq_default_affinity()
static void __init init_irq_default_affinity(void)
{
if (!cpumask_available(irq_default_affinity))
zalloc_cpumask_var(&irq_default_affinity, GFP_NOWAIT);
if (cpumask_empty(irq_default_affinity))
cpumask_setall(irq_default_affinity); /* IRQ 中断 的 CPU 亲和性默认值 为 【系统中所有 CPU】 */
}
#ifdef CONFIG_CPUMASK_OFFSTACK
/* Assuming NR_CPUS is huge, a runtime limit is more efficient. Also,
* not all bits may be allocated. */
#define nr_cpumask_bits nr_cpu_ids
#else
#define nr_cpumask_bits ((unsigned int)NR_CPUS)
#endif
static inline void cpumask_setall(struct cpumask *dstp)
{
bitmap_fill(cpumask_bits(dstp), nr_cpumask_bits);
}
4. 硬件架构 CPU 固有 IRQ 中断亲和性
本文以 ARMv7
架构 CPU 为例,简要说明其固有 16
个 IRQ 中断的 CPU 亲和性的配置过程:
start_kernel()
early_irq_init()
/* kernel/irq/irqdesc.c */
int __init early_irq_init(void)
{
int i, initcnt, node = first_online_node;
struct irq_desc *desc;
init_irq_default_affinity();
/* Let arch update nr_irqs and return the nr of preallocated irqs */
initcnt = arch_probe_nr_irqs(); /* 获取架构 CPU 支持的 IRQ 中断数目 */
printk(KERN_INFO "NR_IRQS: %d, nr_irqs: %d, preallocated irqs: %d\n",
NR_IRQS, nr_irqs, initcnt);
...
for (i = 0; i < initcnt; i++) {
desc = alloc_desc(i, node, 0, NULL, NULL); /* 分配 irq_desc 并初始化:包括 CPU 亲和性设置 */
set_bit(i, allocated_irqs); /* 标记 IRQ 中断号 @i 已分配 */
irq_insert_desc(i, desc); /* 将 IRQ 中断 @i 的描述符插入描述符基树 @irq_desc_tree */
}
...
}
static struct irq_desc *alloc_desc(int irq, int node, unsigned int flags,
const struct cpumask *affinity,
struct module *owner)
{
struct irq_desc *desc;
/* 分配一个 IRQ 中断描述符 */
desc = kzalloc_node(sizeof(*desc), GFP_KERNEL, node);
...
/*
* 分配用来记录 IRQ 中断 @desc 的 CPU 亲和性掩码 的 空间,包括:
* desc->irq_common_data.affinity
* desc->irq_common_data.effective_affinity (需开启 CONFIG_GENERIC_IRQ_EFFECTIVE_AFF_MASK)
*/
if (alloc_masks(desc, node))
goto err_kstat;
...
desc_set_defaults(irq, desc, node, affinity, owner);
...
}
static void desc_set_defaults(unsigned int irq, struct irq_desc *desc, int node,
const struct cpumask *affinity, struct module *owner)
{
...
desc_smp_init(desc, node, affinity);
}
static void desc_smp_init(struct irq_desc *desc, int node,
const struct cpumask *affinity)
{
if (!affinity)
affinity = irq_default_affinity; /* IRQ 使用默认亲和性,即使用 系统中所有 CPU */
cpumask_copy(desc->irq_common_data.affinity, affinity);
...
}
5. 中断芯片 各中断 CPU 亲和性 初始化
本文以 ARM GIC v2
中断芯片为例,分别说明下面 3 类中断 CPU 亲和性的设置过程:
SGI(Software-generated interrupt):用于处理期之间的通信,编号区间为 0~15 ,对于每个处理器,都是相同的编号区间;
PPI(Private peripheral interrupt):处理器私有中断,编号区间为 16~31 ,处理器私有编号,每个处理器都是相同的编号区间;
SPI(Shared peripheral interrupt):全局中断,可发送给任一处理器,编号区间为 32~1019 ,且编号全局唯一。
对于 SGI 中断,在发送指定接收的 CPU,不需要亲和性的配置;对于 PPI 中断,是每个 CPU 私有的,也不需要亲和性配置;只有 SPI 中断,是全局共享的,可以发送给任意 CPU,需要进行亲和性配置。
5.1 GIC v2 芯片的 SPI 中断 CPU 亲和性 初始化
对于中断的 CPU 亲和性配置,也有两个层次:软件层次
,硬件中断芯片层次
(本文指 GIC v2)。
5.1.1 软件层次: 中断 CPU 亲和性 初始化
通用层次,是和具体中断芯片无关的、Linux 内核通用层次的配置。来看一个 I2C 总线中断 CPU 亲和性配置过程的例子。I2C 的中断相关 DTS 配置如下:
i2c0: i2c@ff040000 {
...
interrupts = <GIC_SPI 11 IRQ_TYPE_LEVEL_HIGH>; // I2C 中断
...
};
在 I2C 总线驱动加载过程中,解析并初始化中断配置(只重点关注中断 CPU 亲和性的配置过程):
irq = platform_get_irq(pdev, 0);
of_irq_get(dev->dev.of_node, num);
...
rc = of_irq_parse_one(dev, index, &oirq); /* 解析 DTS 中断配置 */
...
return irq_create_of_mapping(&oirq);
return irq_create_fwspec_mapping(&fwspec);
virq = irq_domain_alloc_irqs(domain, 1, NUMA_NO_NODE, fwspec);
__irq_domain_alloc_irqs(domain, -1, nr_irqs, node, arg, false, NULL)
virq = irq_domain_alloc_descs(irq_base, nr_irqs, 0, node, affinity);
virq = __irq_alloc_descs(-1, hint, cnt, node, THIS_MODULE, affinity);
ret = alloc_descs(start, cnt, node, affinity, owner);
desc = alloc_desc(start + i, node, flags, mask, owner);
...
可以看到,解析 DTS 中断配置的过程,最后调用了 alloc_desc()
,这里 alloc_desc()
的流程,同前面 4. 硬件架构 CPU 固有 IRQ 中断亲和性
的分析,所以,这里设置的中断亲和性也是系统中所有 CPU 集合
。
5.1.2 中断芯片层次: GIC v2 中断 CPU 亲和性 初始化
所有的中断 CPU 亲和性的设置,最终都要落实到硬件层次的具体中断芯片才有意义,毕竟中断转发给哪些 CPU 处理,是中断芯片控制的。本文以 GIC v2 中断芯片为例,简要说明中断 CPU 亲和性硬件层次的设置过程。GIC 包含两大部分:Distributor
和 CPU Interface
两大部分,所以中断 CPU 亲和性的设置,也是对它们进行配置。这部分的细节不在本文展开,感兴趣的读者可参考博文 Linux: 中断实现简析 章节 3.2.2 GIC芯片初始化
对函数 gic_dist_init()
和 gic_cpu_init()
的分析:gic_dist_init()
初始化了 Distributor
,gic_cpu_init()
初始化了 CPU Interface
。
6. 中断芯片各中断 CPU 亲和性 修改
系统运行过程中,在中断 CPU 亲和性初始化后,也可以通过系统提供的接口对中断 CPU 亲和性进行修改。
Linux 内核提供下面两个文件节点,允许用户空间修改 IRQ 中断的 CPU 亲和性:
/proc/irq/<N>/smp_affinity # IRQ N 的 CPU 亲和性,以十六进制掩码形式配置接口,如 f
/proc/irq/<N>/smp_affinity_list # smp_affinity 列表形式配置接口,如 0-3
其中 <N>
为 Linux 中断号
(非硬件中断号),通过对 smp_affinity
的写入来修改中断的 CPU 亲和性。如:
# echo 3 > /proc/irq/<N>/smp_affinity ## 将中断 N 的亲和性设置为 CPU 0,1
其设置流程为:
/* kernel/irq/proc.c */
static ssize_t write_irq_affinity(int type, struct file *file,
const char __user *buffer, size_t count, loff_t *pos)
{
unsigned int irq = (int)(long)PDE_DATA(file_inode(file));
cpumask_var_t new_value;
int err;
/* 新的 CPU 亲和性掩码值 */
if (!alloc_cpumask_var(&new_value, GFP_KERNEL))
return -ENOMEM;
/*
* @type == 0: /proc/irq/<N>/smp_affinity_list
* @type == 1: /proc/irq/<N>/smp_affinity
*/
if (type) /* @buffer 参数列表形式: 如 0-3 */
err = cpumask_parselist_user(buffer, count, new_value);
else /* @buffer 参数掩码形式: 如 f */
err = cpumask_parse_user(buffer, count, new_value);
if (!cpumask_intersects(new_value, cpu_online_mask)) {
/*
* Special case for empty set - allow the architecture code
* to set default SMP affinity.
*/
/* 非正常设置,最终会走到 irq_do_set_affinity() */
err = irq_select_affinity_usr(irq) ? -EINVAL : count;
} else { /* 按用户指定亲和掩码值进行配置 */
irq_set_affinity(irq, new_value);
err = count;
}
...
}
/* include/linux/interrupt.h */
static inline int
irq_set_affinity(unsigned int irq, const struct cpumask *cpumask)
{
return __irq_set_affinity(irq, cpumask, false);
}
/* kernel/irq/manage.c */
int __irq_set_affinity(unsigned int irq, const struct cpumask *mask, bool force)
{
struct irq_desc *desc = irq_to_desc(irq);
...
raw_spin_lock_irqsave(&desc->lock, flags);
ret = irq_set_affinity_locked(irq_desc_get_irq_data(desc), mask, force);
raw_spin_unlock_irqrestore(&desc->lock, flags);
...
}
int irq_set_affinity_locked(struct irq_data *data, const struct cpumask *mask,
bool force)
{
struct irq_chip *chip = irq_data_get_irq_chip(data);
struct irq_desc *desc = irq_data_to_desc(data);
...
if (irq_can_move_pcntxt(data)) {
ret = irq_do_set_affinity(data, mask, force);
} else {
...
}
...
}
int irq_do_set_affinity(struct irq_data *data, const struct cpumask *mask,
bool force)
{
struct irq_desc *desc = irq_data_to_desc(data);
struct irq_chip *chip = irq_data_get_irq_chip(data);
...
/* 在中断芯片 (GIC v2) 设置 中断 的 CPU 亲和性 */
ret = chip->irq_set_affinity(data, mask, force); /* gic_set_affinity(), ... */
switch (ret) {
case IRQ_SET_MASK_OK:
case IRQ_SET_MASK_OK_DONE:
cpumask_copy(desc->irq_common_data.affinity, mask); /* 更新 中断 的 CPU 亲和性掩码 affinity */
case IRQ_SET_MASK_OK_NOCOPY:
irq_validate_effective_affinity(data);
irq_set_thread_affinity(desc); /* 保存 中断线程的 CPU 亲和性设置 */
ret = 0;
}
...
}
/* drivers/irqchip/irq-gic.c */
#ifdef CONFIG_SMP
static int gic_set_affinity(struct irq_data *d, const struct cpumask *mask_val,
bool force)
{
...
if (!force)
cpu = cpumask_any_and(mask_val, cpu_online_mask);
else
cpu = cpumask_first(mask_val);
...
gic_lock_irqsave(flags);
mask = 0xff << shift;
/*
* 用户可能以为是根据 @mask_val 来设置中断派发配置值,但是代码
* 是从 gic_cpu_map[@cpu] 来取派发配置值!!!
* gic_cpu_map[] 是每个 CPU 在启动期间初始化好的值,之后
* 就不会再改变,它的初始值是只会将中断派发给 BOOT CPU (通常是 CPU 0)。
*/
bit = gic_cpu_map[cpu] << shift;
val = readl_relaxed(reg) & ~mask;
writel_relaxed(val | bit, reg);
gic_unlock_irqrestore(flags);
/* 更新中断亲和性的 effecive_affinity 掩码 */
irq_data_update_effective_affinity(d, cpumask_of(cpu));
return IRQ_SET_MASK_OK_DONE;
}
#endif
/* include/linux/irq.h */
static inline void irq_data_update_effective_affinity(struct irq_data *d,
const struct cpumask *m)
{
cpumask_copy(d->common->effective_affinity, m);
}
/* kernel/irq/manage.c */
void irq_set_thread_affinity(struct irq_desc *desc)
{
struct irqaction *action;
for_each_action_of_desc(desc, action)
if (action->thread)
set_bit(IRQTF_AFFINITY, &action->thread_flags);
}
从上面的代码流程分析可以看到,中断 CPU 亲和性的设置,其实也分软件
和硬件
两个层次。软件上,更新了中断的两个掩码 desc->irq_common_data.affinity
和 desc->irq_common_data.effective_affinity
,同时如果中断线程化的话,还会设置相关中断线程的 CPU 亲和性(注意,这里仅仅是记录);而硬件层次是设置中断在中断芯片(GIC v2)上的 CPU 亲和性。
对于中断的 CPU 亲和性设置,还剩下最后一个片段,那就是在中断线程化处理场景,将中断线程绑定到 irq_set_thread_affinity()
记录的 CPU 集合上:
/* kernel/irq/manage.c */
request_threaded_irq()
retval = __setup_irq(irq, desc, action);
if (new->thread_fn && !nested) {
ret = setup_irq_thread(new, irq, false);
}
static int
setup_irq_thread(struct irqaction *new, unsigned int irq, bool secondary)
{
struct task_struct *t;
struct sched_param param = {
.sched_priority = MAX_USER_RT_PRIO/2, /* 中断线程使用实时优先级 */
};
if (!secondary) {
t = kthread_create(irq_thread, new, "irq/%d-%s", irq,
new->name);
} else {
t = kthread_create(irq_thread, new, "irq/%d-s-%s", irq,
new->name);
param.sched_priority -= 1;
}
...
sched_setscheduler_nocheck(t, SCHED_FIFO, ¶m);
/*
* We keep the reference to the task struct even if
* the thread dies to avoid that the interrupt code
* references an already freed task_struct.
*/
get_task_struct(t);
new->thread = t;
/*
* Tell the thread to set its affinity. This is
* important for shared interrupt handlers as we do
* not invoke setup_affinity() for the secondary
* handlers as everything is already set up. Even for
* interrupts marked with IRQF_NO_BALANCE this is
* correct as we want the thread to move to the cpu(s)
* on which the requesting code placed the interrupt.
*/
set_bit(IRQTF_AFFINITY, &new->thread_flags);
return 0;
}
/* kernel/irq/manage.c */
/*
* Interrupt handler thread
*/
static int irq_thread(void *data)
{
...
irq_thread_check_affinity(desc, action);
while (!irq_wait_for_interrupt(action)) {
...
irq_thread_check_affinity(desc, action);
action_ret = handler_fn(desc, action);
...
}
...
}
#ifdef CONFIG_SMP
/*
* Check whether we need to change the affinity of the interrupt thread.
*/
static void
irq_thread_check_affinity(struct irq_desc *desc, struct irqaction *action)
{
cpumask_var_t mask;
bool valid = true;
if (!test_and_clear_bit(IRQTF_AFFINITY, &action->thread_flags))
return;
/*
* In case we are out of memory we set IRQTF_AFFINITY again and
* try again next time
*/
if (!alloc_cpumask_var(&mask, GFP_KERNEL)) {
set_bit(IRQTF_AFFINITY, &action->thread_flags);
return;
}
raw_spin_lock_irq(&desc->lock);
/*
* This code is triggered unconditionally. Check the affinity
* mask pointer. For CPU_MASK_OFFSTACK=n this is optimized out.
*/
if (cpumask_available(desc->irq_common_data.affinity))
cpumask_copy(mask, desc->irq_common_data.affinity);
else
valid = false;
raw_spin_unlock_irq(&desc->lock);
if (valid)
set_cpus_allowed_ptr(current, mask); /* 设置 中断线程的 CPU 亲和性 */
free_cpumask_var(mask);
}
#else
static inline void
irq_thread_check_affinity(struct irq_desc *desc, struct irqaction *action) { }
#endif