Linux 中断管理机制
现在多数CPU都是采用中断控制器来管理中断(以往中断少,只要几个寄存器就行,一个bit表示一个类型的中断),Linux使用 irq_dimian的结构表示一个中断控制器。
硬件中断号和Linux软件中断号的映射
中断类型一般分为:
SGI:软件触发中断,通常用于多核之间通讯,硬件中断号从0-15.
PPI:私有外设中断,这是每个处理核心私有的中断,硬件中断号从16-31,。PPI通常会送达到指定的CPU上,应用场景有CPU本地时钟。
SPI:公用的外设中断,支持的数量很多,硬件中断号从32-xxx。
系统初始化的时, customize_machine()函数会去枚举并初始化总线上的设备,最终解析DTS中的相关信息,把相关信息添加到struct device 数据结构中,向linux内核注册一个新的外设。其中就有我们关注的中断相关信息的枚举过程。irq_of_parse_and_map().
例如:解析DTS中串口0设备的硬件中断号,返回linux内核的IRQ中断号,并保存到struct amba_device 数据结构中的irq[]数组中。 串口驱动程序在pl0ll_probe()函数中直接从 dev->irq[0]中获取IRQ中断号。
static int pl0ll_probe(struct amba_device *dev, const struct amba_id *id)
{
...
uap->port.irq = dev->irq[0];
...
}
每一个中断控制器都用一个 struct irq_domain结构来抽象描述,在drivers/irqchip 目录放着中断控制器的驱动代码。
struct irq_domain 结构如下:
struct irq_domain {
struct list_head link; //用于将irq domain挂在全局链表中
const char *name; //该中断控制器名称的名称
const struct irq_domain_ops *ops; //中断控制器操作方法
void *host_data;
unsigned int flags;
/* Optional data */
struct fwnode_handle *fwnode;
enum irq_domain_bus_token bus_token;
struct irq_domain_chip_generic *gc;
#ifdef CONFIG_IRQ_DOMAIN_HIERARCHY
struct irq_domain *parent;
#endif
/* reverse map data. The linear map gets appended to the irq_domain */
irq_hw_number_t hwirq_max; //中断控制器最大支持的硬件中断数
unsigned int revmap_direct_max_irq;
unsigned int revmap_size;
struct radix_tree_root revmap_tree;
unsigned int linear_revmap[];
};
GIC中断控制器在初始化时解析DTS信息中定义了几个GIC控制器,每个GIC控制器注册一个irq_domain数据结构。
回到系统枚举阶段的中断映射过程,在of_amba_device_create()函数中,irq_of_parse_and_map()负责把硬件中断号映射到linux内核的IRQ中断号中。
of_platform_populate() -> of_platform_bus_create() -> of_amba_device_create->irq_of_parse_and_map()
unsigned int irq_of_parse_and_map(struct device_node *dev, int index)
{
struct of_phandle_args oirq;
//将device_node 节点 中的硬件中断号保存在oirq中
if (of_irq_parse_one(dev, index, &oirq))
return 0;
return irq_create_of_mapping(&oirq);
}
unsigned int irq_create_of_mapping(struct of_phandle_args *irq_data)
{
struct irq_fwspec fwspec;
//将irq_data中的数据提取到fwspec里面(包括硬件中断号)
of_phandle_args_to_fwspec(irq_data, &fwspec);
return irq_create_fwspec_mapping(&fwspec);
}
unsigned int irq_create_fwspec_mapping(struct irq_fwspec *fwspec)
{
struct irq_domain *domain;
struct irq_data *irq_data;
irq_hw_number_t hwirq;
unsigned int type = IRQ_TYPE_NONE;
int virq;
if (fwspec->fwnode) {
domain = irq_find_matching_fwspec(fwspec, DOMAIN_BUS_WIRED);
if (!domain)
domain = irq_find_matching_fwspec(fwspec, DOMAIN_BUS_ANY);
} else {
domain = irq_default_domain;
}
/* 上面的代码是用来找到该设备的中断时哪一个中断控制器管理的,并返回该中断控制器的数据结构 */
if (!domain) {
pr_warn("no irq domain found for %s !\n",
of_node_full_name(to_of_node(fwspec->fwnode)));
return 0;
}
//硬件中断号的转换,对于PPI中断,需要加是32才是硬件中断号
if (irq_domain_translate(domain, fwspec, &hwirq, &type))
return 0;
/*
* If the trigger type has not been set yet, then set
* it now and return the interrupt number.
*/
if (irq_get_trigger_type(virq) == IRQ_TYPE_NONE) {
irq_data = irq_get_irq_data(virq);
if (!irq_data)
return 0;
irqd_set_trigger_type(irq_data, type);
return virq;
}
pr_warn("type mismatch, failed to map hwirq-%lu for %s!\n",
hwirq, of_node_full_name(to_of_node(fwspec->fwnode)));
return 0;
}
if (irq_domain_is_hierarchy(domain)) {
//从位图上面分配一个可用的Linux中断号
virq = irq_domain_alloc_irqs(domain, 1, NUMA_NO_NODE, fwspec);
if (virq <= 0)
return 0;
} else {
....
}
....
return virq;
}
irq_domain_alloc_irqs() -> __irq_domain_alloc_irqs().
int __irq_domain_alloc_irqs(struct irq_domain *domain, int irq_base,
unsigned int nr_irqs, int node, void *arg,
bool realloc, const struct cpumask *affinity)
{
int i, ret, virq;
....
//从位图中找到第一个空闲的比特位(对应了软件终端号和irq_desc)
//irq_desc[]数组中定义了NR_IRQS个中断描述符,数组下标表示IRQ中断号,通过IRQ中断号可以找到相应的中断描述符
virq = irq_domain_alloc_descs(irq_base, nr_irqs, 0, node, affinity);
if (virq < 0) {
return virq;
}
if (irq_domain_alloc_irq_data(domain, virq, nr_irqs)) {
ret = -ENOMEM;
goto out_free_desc;
}
mutex_lock(&irq_domain_mutex);
ret = irq_domain_alloc_irqs_recursive(domain, virq, nr_irqs, arg);
if (ret < 0) {
mutex_unlock(&irq_domain_mutex);
goto out_free_irq_data;
}
for (i = 0; i < nr_irqs; i++)
irq_domain_insert_irq(virq + i);
mutex_unlock(&irq_domain_mutex);
return virq;
out_free_irq_data:
irq_domain_free_irq_data(virq, nr_irqs);
out_free_desc:
irq_free_descs(virq, nr_irqs);
return ret;
}
struct irq_desc {
struct irq_data irq_data;
const char *name;
irq_flow_handler_t handle_irq; // PPI中断都是指向hand_fasteoi_irq()
struct irqaction *action; //我们注册的处理函数
....
}
struct irq_data {
u32 mask;
unsigned int irq; //关注
unsigned long hwirq; //关注
struct irq_chip *chip;
struct irq_domain *domain;
....
};
struct irq_desc 数据结构内置了struct irq_data结构体, struct irq_data结构体成员irq指向软件中断号,hwirq指向硬件中断号,如果把这两个成员填写完成,即完成了硬件中断号到软件中断号的映射。
步骤总结:
1、解析DTS文件,初始化中断控制器。
2、解析DTS文件内的设备,向内和注册设备的时候,需要解析设备的中断信息。
3、计算硬件中断号:Linux 根据总线枚举设备的时候,会去解析DTS文件中的设备节点信息,其中就包含了设备在主板上的中断号,并计算出硬件中断号
4、计算软件中断号:从位图里面获取第一个未被使用的终端号。
5、根据软件中断号,获取一个中断描述符结构。
6、在这个中断描述符中,记录下软件中断号和硬件中断号,即完成了映射过程。
中断注册流程
中断注册函数 request_irq()函数如下:
/**
* irq : 软件中断号
* handler : 中断处理回调
*/
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,const char *name, void *dev)
{
return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}
中断线程化是实时Linux项目开发的一个新增特性,目的是降低中断处理对系统实时延时的影响。Linux内核已经把中断处理分成上下半部,为什么还需要引入中断线程化机制呢?
在Linux内核里,中断具有最高优先级,只要中断发生,内核会暂停手头的工作转向中断处理,等到所有挂起等待(pending)的中断和软中断处理完毕后才会执行进程调度,因此这个过程会造成实时任务得不到及时处理。中断上下文总是强占进程上下文,中断上下文不仅是中断处理程序,还包括Softirq软中断,tasklet等。
中断线程化的目的是把中断处理中一些繁重的任务作为内核线程来运行,实时进程可以有比中断线程更高的优先级,这样高优先级实时进程就可以得到优先处理。
扩展: 如何解决中断反转:
方式一:优先级天花板
当线程申请某共享资源时,把该线程的优先级提升到可访问这个资源的所有线程中的最高优先级,这个优先级成为该资源的优先级天花板,这种方法简单易行,不必进行复杂的判断,不管线程是否阻塞了高优先级线程的运行,只要线程访问共享资源都会提升线程的优先级
方式二:优先级继承
优先级继承是当线程A申请资源source时,如果共享资源source正在被线程C使用,通过对比线程C与自身的优先级,如果发现线程C的优先级小于自身的优先级,则将线程C的优先级提升到自身的优先级,线程C释放资源source以后,再恢复线程C的优先级。这种方式只在占有资源的优先级线程阻塞了高优先级线程时才动态的改变线程的优先级。
int request_threaded_irq(unsigned int irq, irq_handler_t handler,
irq_handler_t thread_fn, unsigned long irqflags,
const char *devname, void *dev_id)
{
struct irqaction *action;
struct irq_desc *desc;
int retval;
if (irq == IRQ_NOTCONNECTED)
return -ENOTCONN;
desc = irq_to_desc(irq);//通过软件中断号找到中断描述符
if (!desc)
return -EINVAL;
if (!irq_settings_can_request(desc) ||
WARN_ON(irq_settings_is_per_cpu_devid(desc)))
return -EINVAL;
if (!handler) {
if (!thread_fn)
return -EINVAL;
handler = irq_default_primary_handler;
}
action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
if (!action)
return -ENOMEM;
//初始化action以后,会吧action放在 中断描述结构里面
action->handler = handler;
action->thread_fn = thread_fn;
action->flags = irqflags;
action->name = devname;
action->dev_id = dev_id;
chip_bus_lock(desc);
retval = __setup_irq(irq, desc, action);
chip_bus_sync_unlock(desc);
return retval;
}
这样我们的中断就初始化完成了,当然__setup_irq函数里面还做了中断线程化的处理。
中断响应–ARM层
当CPU核心感知到中断发生以后,硬件会自动做如下一些事情
1、保存中断发生时CPSR寄存器的内容到SPSR_irq寄存器中
2、保存返回地址到LR_irq寄存器中。
3、修改CPSR寄存器,进入IRQ模式。
4、跳转到中断向量表的IRQ向量中。
中断响应-软件层
软件需要做的事情从中断向量表开始。
1、修改CPSR寄存器,进入SVC模式,
2、保存中断现场(R0-R14寄存器),这时的栈已经是当前进程(发生中断的进程)的内核栈,所以寄存器的内容保存在当前进程的栈中,返回时也是从这个栈中恢复。
3、跳到GIC控制器的中断处理中
irq_handle->gic_handle_irq()[这个函数里面会读取当前发生中断是哪个硬件终端号]->
handle_domain_irq()[通过硬件中断号,找到软件中断号]->generic_handle_irq()->handle_fasteoi_irq() -> action->handle