Linux中的中断管理机制_linux 另一个处理器的中断

GIC中断控制器主要由两个部分组成,分别是 仲裁单元(distributor ,为每一个中断源维护一个状态机,支持inactive、pending、active和active and pending状态)CPI接口模块(CPU Interface)

2、GIC检测中断流程

当发生一个外设中断后

  • a: 一个中断M产生,发生了电平变化,被中断控制器中的仲裁单元检测到了(仲裁单元检测中断信号
  • b: 然后仲裁单元会把这个中断M的状态设置为pending(等待状态)(仲裁单元将中断信号设置为pending状态
  • c: 然后过了一段时间后,中断控制器中的CPU Interface模块会把nFIQCPU[n]信号拉低,目的是向CPU报告中断请求,然后将中断M的硬件中断号存放到GICC_IAR寄存器中(CPU Interface向CPU报告中断请求
  • d: 如果这个时候有一个更高优先级的中断N来了,由于中断控制器支持优先级抢占功能,所以这个时候N会变成当前CPU所有pending状态下优先级最高的中断(意味着它会被优先分配)。(发生中断抢占
  • e: 然后跟步骤c一样,过了一段时间后,CPU Interface模块会再次去把nFIQCPU[n]信号拉低,由于这个时候nFIQCPU[n]已经是低电平了,所以只需要更新GICC_IAR寄存器的值为中断N的硬件中断号(设置新的优先级较高的中断信号
  • f: 然后CPU就会去读取GICC_IAR寄存器,把寄存器中的硬件中断号读出来,也就相当于是响应中断了。这个时候仲裁单元就会把中断N的状态从pending变成了activce and pending。(响应中断信号
  • g: 然后后面就是Linux内核处理中断N的中断服务程序了(处理中断
  • h: 在Linux中断处理中断N的过程中,CPU Interface模块会重新拉高nFIQCPU[n]信号,当中断N处理完成后,会将N的硬件中断号写入到GICC_EOIR寄存器中,表示完成中断N的全部处理过程。
  • i: 然后中断控制器的仲裁单元,会重新选择该CPU下pending状态的中断中优先级最高的一个,发送该中断请求给CPU Interface模块,继续前面的流程。

在这里插入图片描述

3、中断注册

先由两个问题来开启这个问题的讨论

1、用户是怎么使用中断的? 使用request_irq/request_threaded_irq向内核注册中断

2、request_irq/request_threaded_irq使用的irq中断号是软件中断号,它是怎么来的? 它又是如何映射到具体的硬件设备的?
(其实request_irq函数调用的也是request_threaded_irq函数,只是传参的时候线程处理函数thread_fn函数设置成NULL)

3.1、request_irq

request_irq函数原型如下:

static inline int __must_check 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);
}

  • irq:IRQ中断号,这里使用的是软件中断号,不是硬件中断号
  • handler:中断处理函数
  • thread_fn:中断线程化的处理函数。如果这个参数不为NULL,就会创建一个内核线程
  • irqflags:中断标志位
  • devname:该中断名称
  • dev_id:传递给中断处理程序的参数

request_irq函数的调用关系如下:

request_irq
	request_threaded_irq	1--根据获取到的软件中断号, 向内核注册中断
		irq_to_desc				2--根据传进来的软件中断号,获取中断描述符irq_desc
			action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);		3--填充一个irqaction结构体
			action->handler = handler;									4--将设备驱动中的handler填充到action->handle中
			action->thread_fn = thread_fn;
			action->flags = irqflags;
			action->name = devname;
			action->dev_id = dev_id;
		
		__setup_irq		5--对中断请求进行判断,是否线程化等....

在这里插入图片描述

3.2、request_threaded_irq

request_threaded_irq被称为 中断线程化,明明已经有了request_irq了,为什么还需要request_threaded_irq为什么需要将中断线程化??

中断线程化目的是为了降低中断处理对系统实时延迟的影响。 可以这么说会比较难理解,我们来看进一步的解释。在Linux内核中,中断具有最高的优先级,只要有中断发生,内核会暂停手头的工作去处理中断时间,只有所有挂起等待的中断和软中断都处理完毕后才会继续回去执行中断前的任务,因此这个过程会造成实时任务的不及时处理。中断线程化的目的就是把中断处理中的一些繁重的任务作为内核线程来运行,让实时进程能够有比中断线程更高的优先级,让实时进程可以得到优先处理。

稍微了解了中断的注册,这个时候又有一个问题:那刚才说的request_irq/request_threaded_irq中使用的irq参数是虚拟中断号,但是在DTS中配置的是硬件中断号,它们之间是怎么转换的呢? 这就涉及到中断号之间的映射了。

4、硬件中断号和Linux中断号的映射

接着上面的内容,我们都知道了Linux驱动中,注册中断的API函数request_irq()/request_threaded_irq()是使用了Linux内核软件中断号(俗称软件中断号或者IRQ中断号),而不是硬件中断号

4.1、中断在DTS中的配置

我们先来看看中断在DTS中的配置是什么样的,对ARM平台来说,所有与硬件相关的信息都是采用DTS的方式进行配置,看下面例子:

pio: 1000b000.pinctrl {
                compatible = "mediatek,mt6771-pinctrl";
                reg_bases = <&gpio>,
                        <&iocfg_0>,
                        <&iocfg_1>,
                        <&iocfg_2>,
                        <&iocfg_3>,
                        <&iocfg_4>,
                        <&iocfg_5>,
                        <&iocfg_6>,
                        <&iocfg_7>;
                reg_base_eint = <&eint>;
                pins-are-numbered;
                gpio-controller;
                gpio-ranges = <&pio 0 0 191>;
                #gpio-cells = <2>;
                interrupt-controller;
                #interrupt-cells = <4>;
                interrupts = <GIC_SPI 177 IRQ_TYPE_LEVEL_HIGH>;
        };

上面是一个名为pio的节点,我们看看这个节点中跟中断相关的信息都是什么意思:

  • interrupts
    我们可以看到,对于这个节点来说,interrupt = <GIC_SPI 177 IRQ_TYPE_LEVEL_HIGH>,这个代表的是什么意思呢?看过上面介绍的童鞋应该可以猜到,第一个参数GIC_SPI指的是,这个中断属于外设中断(SPI),后面的177代表的是中断号代表的是实际上的物理中断,但是由于该中断属于SPI中断,而SPI的中断号的范围是ID32~IDX,所以实际的中断号应该是177+32=209,第三个参数指的是中断的触发类型
  • interrupt-controller
    拥有这个属性的设备表示是一个中断控制器
  • interrupt-cells
    用于指定编码一个中断源所需要的单元个数

4.2、硬件中断号和软件中断号的映射过程

我们先来了解一下大概的函数调用过程,然后后面再来一一解释用到的核心的结构体

在Linux内核驱动中,我们一般使用irq_of_parse_and_map这个函数作为入口,来实现硬件中断号到虚拟中断号的映射,我们看看它是如何一步一步调用其他函数,最终实现映射功能的。(在Linux内核中,也有其他函数同样可以实现中断号的映射功能,比如platform_get_irq_byname,不过最终调用到的函数都是一样的,所以在这里我们就以 irq_of_parse_and_map 为例来进行分析就可以了)

4.2.1、映射入口函数irq_of_parse_and_map
  • 函数原型unsigned int irq_of_parse_and_map(struct device_node *dev, int index)
unsigned int irq\_of\_parse\_and\_map(struct device\_node \*dev, int index)
{
	struct of\_phandle\_args oirq;

	if (of\_irq\_parse\_one(dev, index, &oirq))
		return 0;

	return irq\_create\_of\_mapping(&oirq);
}

总的调用框图如下,可结合这张图看下下面的描述:
在这里插入图片描述

在这里插入图片描述

4.2.2、中断管理核心的数据结构

4.2.2.1、irq_desc
  • struct irq_desc

Linux中每一个产生的中断都会使用一个irq_desc结构体来描述,我们来看看这个irq_desc结构体里面都有什么内容

struct irq\_desc {
	struct irq\_common\_data	irq_common_data;
	struct irq\_data		irq_data;
	unsigned int __percpu	\*kstat_irqs;
	irq\_flow\_handler\_t	handle_irq;
	struct irqaction	\*action;	/\* IRQ action list \*
 .........
}

4.2.2.2、irq_data
  • struct irq_data

可以看到,irq_desc结构体中,有一个比较重要的结构体,irq_data,我们来看看它里面又有啥

struct irq\_data {
	u32			mask;
	unsigned int		irq;
	unsigned long		hwirq;
	struct irq\_common\_data	\*common;
	struct irq\_chip		\*chip;
	struct irq\_domain	\*domain;
	void			\*chip_data;
};

irq结构体里面的成员相信大家都不会陌生

+ 1) `irq`:软件中断号
+ 2) `hwirq`:硬件中断号
+ 3)`irq_chip`:代表的是对硬件中断器的操作
+ 4)`irq_domain`:是一个中断控制器的抽象描述,主要任务是完成硬件中断号到Linux软件中断号的映射
4.2.2.3、irq_chip
  • struct irq_chip

如上所说,irq_chip结构体就是包含了一系列对硬件中断器的操作函数

struct irq\_chip {
	struct device	\*parent_device;
	const char	\*name;
	unsigned int	(\*irq_startup)(struct irq\_data \*data);
	void		(\*irq_shutdown)(struct irq\_data \*data);
	void		(\*irq_enable)(struct irq\_data \*data);
	void		(\*irq_disable)(struct irq\_data \*data);
	........
}

4.2.2.4、irq_domain
  • struct irq_domain

一个中断控制器用一个irq_domain数据结构来抽象描述,以前SoC内部的中断管理比较简单,通常有一个全局的中断状态寄存器,每个比特位管理一个外设中断,直接简单的映射硬件中断号到Linux IRQ中断号即可。但是,随着Soc的发展,SoC内部包含了多个中断控制器,比如传统的中断控制器GICGPIO类型的中断控制器等。面对如此复杂的硬件,导致之前的Linux内核管理机制无法较好得管理,这个时候就没办法沿用之前的一一对应的中断管理方式,因为多个中断控制器,可能存在相同的硬件中断号,当需要映射为Linux系统中的虚拟中断号时,不知道这个硬件中断号到底是哪个中断控制器发出的,所以在Linux 3.1内核就引入了 IRQ domain 这一个概念。简单点来说,IRQ domain结构体是一个包含一些将硬件中断号转换为虚拟中断号的函数的一个东东

struct irq\_domain {
	struct list\_head link;
	const char \*name;
	const struct irq\_domain\_ops \*ops;
	void \*host_data;
	unsigned int flags;
	unsigned int mapcount;

	/\* Optional data \*/
	struct fwnode\_handle \*fwnode;
	enum irq\_domain\_bus\_token bus_token;
	struct irq\_domain\_chip\_generic \*gc;

	/\* 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[];
};

  • irq_domain_ops

我们前面说到,irq_domain是一个中断控制器的抽象描述,它的作用就是实现硬件中断号到Linux软件中断号的映射,那么这个映射是由irq_domain中的哪个成员来实现的呢?答案就是irq_domain_ops,它里面包含了一系列的操作函数

struct irq\_domain\_ops {
	int (\*match)(struct irq\_domain \*d, struct device\_node \*node, enum irq\_domain\_bus\_token bus_token);
	int (\*select)(struct irq\_domain \*d, struct irq\_fwspec \*fwspec, enum irq\_domain\_bus\_token bus_token);
	int (\*map)(struct irq\_domain \*d, unsigned int virq, irq\_hw\_number\_t hw);
	void (\*unmap)(struct irq\_domain \*d, unsigned int virq);
	int (\*xlate)(struct irq\_domain \*d, struct device\_node \*node, const u32 \*intspec, unsigned int intsize, unsigned long \*out_hwirq, unsigned int \*out_type);
}

4.2.2.5、irq_desc结构体概览图

上面所描述的结构体之间的关系可以看下面的概览图:
在这里插入图片描述

4.2.3、中断映射过程中核心流程的具体实现

有兴趣的同学可以进一步看这一块内容,如果只是想了解大概框架的话,这一块内容可以跳过

既然要看硬件中断号是怎么映射成软件中断号的,而中断控制器又是负责这一项工作的,当然就得从中断控制器看起

5、中断控制器

5.1、中断控制器的DTS配置

在ARM平台上,中断控制器的硬件信息都是通过DTS来进行描述的,我们先来看看一个中断控制器在DTS中是怎么配置的,以联发科的MT8788平台为例,GIC的dts节点内容如下:

gic: interrupt-controller@0c000000 {
                compatible = "arm,gic-v3";
                #interrupt-cells = <3>;
                #address-cells = <2>;
                #size-cells = <2>;
                #redistributor-regions = <1>;
                interrupt-parent = <&gic>;
                interrupt-controller;
                reg = <0 0x0c000000 0 0x40000>, // distributor
                      <0 0x0c100000 0 0x200000>, // redistributor
                      <0 0x0c530a80 0 0x50>; // intpol
                interrupts = <GIC_PPI 9 IRQ_TYPE_LEVEL_HIGH>;
        };

5.2、查找对应驱动代码

通过compatible属性,我们可以找到这个gic的中断控制器对应的驱动代码是哪个——irq-gic-v3.c

  • 代码入口
  IRQCHIP\_DECLARE(gic_v3, "arm,gic-v3", gicv3_of_init);

  #define IRQCHIP\_DECLARE(name, compat, fn) OF\_DECLARE\_2(irqchip, name, compat, fn)

  #define OF\_DECLARE\_2(table, name, compat, fn) \_OF\_DECLARE(table, name, compat, fn, of\_init\_fn\_2)

  #define \_OF\_DECLARE(table, name, compat, fn, fn\_type) \
 static const struct of\_device\_id \_\_of\_table\_##name \
 \_\_used \_\_section(\_\_##table##\_of\_table) \
 = { .compatible = compat, \
 .data = (fn == (fn\_type)NULL) ? fn : fn }

所以IRQCHIP_DECLARE这个函数已经定义了驱动的compatible是arm,gic-v3,和DTS中匹配上后, 就会调用gicv3_of_init函数

6、中断中的上下半部机制

6.1、上下半部机制产生的原因

什么是中断? 比如你现在肚子饿了,然后叫了个外卖,叫完外卖后你肯定不会傻傻坐在那里等电话响,而是会去干其他事情,比如看电视、看书等等,然后等外卖小哥打电话来,你再接听了电话后才会停止看电视或者看书,进而去拿外卖,这就是一个中断过程。你就相当于CPU,而外卖小哥的电话就相当于一个硬件中断。

什么是中断上下部? 继续参考上面的例子,比如你现在叫的是两份外卖,一份披萨一份奶茶,由两个不同的外卖小哥配送,当第一个送披萨的外卖小哥打电话来的时候(硬件中断来了),你接起电话,跟他聊起了恋爱心得,越聊越欢,但是在你跟披萨小哥煲电话粥的时候,奶茶外卖小哥打电话来了,发现怎么打也打不进来,干脆就把你的奶茶喝了,这样你就痛失了一杯奶茶,这个就叫做中断缺失。所以我们必须保证中断是快速执行快速结束的。 那有什么办法可以保护好你的奶茶呢?当你接到披萨小哥的电话后,你跟他说,我知道外卖来了,等我下楼的时候我们面对面吹水,电话先挂了,不然奶茶小哥打不进来,这个就叫做 中断上半部处理,然后等你下楼见到披萨小哥后,面对面吹水聊天,这个就叫做 中断下半部处理,这样在你和披萨小哥聊天的过程中,手机也不占线,奶茶小哥打电话过来你就可以接到了。所以我们一般在中断上半部处理比较紧急的时间(接披萨小哥的外卖),然后在中断下半部处理执行时间比较长的时间(和披萨小哥聊天),把中断分成上下半部,也可以保证后面的中断过来不会发生中断缺失

上半部通常是完成整个中断处理任务中的一小部分,比如硬件中断处理完成时发送EOI信号给中断控制器等,就是在上半部完成的,其他计算时间比较长的数据处理等,这些任务可以放到中断下半部来执行。Linux内核并没有严格的规则约束究竟什么样的任务应该放到下半部来执行,这是要驱动开发者来决定的。中断任务的划分对系统性能会有比较大的影响。

那下半部具体在什么时候执行呢? 这个没有确定的时间点,一般是从硬件中断返回后的某一个时间点内会被执行。下半部执行的关键点是允许响应所有的中断,是一个开中断的环境。 下半部的中断常见的包括软中断tasklet工作队列

6.2、SoftIRQ 软中断

目前驱动中只有块设备和网络子系统使用了软中断,目前Linux内核开发者不希望用户再扩展新的软中断类型,如有需要,建议使用tasklet机制(后面会介绍)。

6.2.1、软中断类型

目前已经定义到的软中断类型如下:(可与看到定义的是枚举类型,索引号越小,软中断优先级越高

enum
{
	HI_SOFTIRQ=0,			//优先级为0,是最高优先级的软中断类型
	TIMER_SOFTIRQ,			//优先级为1,用于定时器的软中断
	NET_TX_SOFTIRQ,			//优先就为2,用于发送网络数据包的软中断
	NET_RX_SOFTIRQ,			//优先级为3,用于接收网络数据包的软中断
	BLOCK_SOFTIRQ,			//优先级为4,用于块设备的软中断
	IRQ_POLL_SOFTIRQ,		
	TASKLET_SOFTIRQ,		//优先级为6,专门为tasklet机制准备的软中断
	SCHED_SOFTIRQ,			//优先级为7,进程调度以及负载均衡
	HRTIMER_SOFTIRQ, 		//优先级为8,高精度定时器
	RCU_SOFTIRQ,    		//优先级为9,专门为RCU服务的软中断

	NR_SOFTIRQS
};

6.2.2、描述软中断的结构体

系统中定义了一个用来描述软中断的数据结构struct softirq_action,并且定义了软中断描述符数组softirq_desc[],每个软中断类型对应一个描述符,其中软中断的索引号就是该数组的索引

6.2.3、软中断的接口
  • open_softirq()函数:注册一个软中断
void open\_softirq(int nr, void (\*action)(struct softirq\_action \*))

nr:代表你希望注册的这个软中断是什么类型的(优先级是多少)
softirq_action:用来描述软中断的结构体

  • raise_softirq()函数:主动触发一个软中断的API接口函数

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以点击这里获取!

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 22
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值