Linux 中断的 CPU 亲和性

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 包含两大部分:DistributorCPU Interface 两大部分,所以中断 CPU 亲和性的设置,也是对它们进行配置。这部分的细节不在本文展开,感兴趣的读者可参考博文 Linux: 中断实现简析 章节 3.2.2 GIC芯片初始化 对函数 gic_dist_init()gic_cpu_init() 的分析:gic_dist_init() 初始化了 Distributorgic_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.affinitydesc->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, &param);

	/*
	 * 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
  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值