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


上面是一个名为`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);

}


总的调用框图如下,可结合这张图看下下面的描述:  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210317201536464.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3d5dHRSYWlu,size_16,color_FFFFFF,t_70)


![在这里插入图片描述](https://img-blog.csdnimg.cn/20210319200950836.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3d5dHRSYWlu,size_16,color_FFFFFF,t_70#pic_center)


### 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内部包含了多个中断控制器,比如传统的`中断控制器GIC`,`GPIO类型的中断控制器`等。面对如此复杂的硬件,导致之前的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结构体概览图


上面所描述的结构体之间的关系可以看下面的概览图:  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210316193043435.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3d5dHRSYWlu,size_16,color_FFFFFF,t_70)


### 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接口函数



void raise_softirq(unsigned int nr)


### 6.3、tasklet


#### 6.3.1、什么是tasklet?


`tasklet`是利用软中断实现的一种下半部机制,本质上是软中断的一个变种,运行在软中断上下文中。`tasklet`由`tasklet_struct`数据结构来描述



struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};

enum
{
TASKLET_STATE_SCHED, /* Tasklet is scheduled for execution */
TASKLET_STATE_RUN /* Tasklet is running (SMP only) */
};


* `next属性`:多个tasklet串成一个链表
* `state属性`:有两个值。`TASKLET_STATE_SCHER`表示tasklet已经被调度,`TASKLET_STATE_RUN`表示tasklet正在运行中
* `count属性`:为0表示tasklet处于激活状态;不为0表示tasklet被禁止,不允许执行
* `func`:tasklet处理程序
* `data`:传递参数给tasklet处理函数


#### 6.3.2、什么时候执行tasklet呢?是在驱动调用了tasklet\_schedule()后马上执行吗?


由于tasklet是基于软中断机制的,一次tasklet\_schedule()后不会马上执行,**要等到软中断被执行时才有机会运行tasklet**。 tasklet挂入到哪个CPU的tasklet\_vet链表,那么就由该CPU的软中断来执行。(`每个CPU会维护两个tasklet链表,一个普通优先级的tasklet_vec,另一个用于高优先级的tasklet_hi_vec`)


#### 6.3.3、tasklet的使用


要像在驱动中使用tasklet,首先需要定义一个tasklet,可以静态声明,可以动态初始化


##### 6.3.3.1、静态定义tasklet



#define DECLARE_TASKLET(name, func, data)
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }

#define DECLARE_TASKLET_DISABLED(name, func, data)
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }


可以看到静态声明tasklet的方式有两个,`DECLARE_TASKLET`和`DECLARE_TASKLET_DISABLED`,它们的区别是`DECLARE_TASKLET`把count初始化为0,**表示tasklet处于激活状态**,而`DECLARE_TASKLET_DISABLED`相反,把count初始化为1,表示**tasklet处于关闭状态。**


##### 6.3.3.2、动态定义tasklet


当然,除了上面说的静态声明tasklet外,也可以在驱动代码中调用`tasklet_init()`函数动态初始化tasklet



void tasklet_init(struct tasklet_struct *t,
void (*func)(unsigned long), unsigned long data)
{
t->next = NULL;
t->state = 0;
atomic_set(&t->count, 0);
t->func = func;
t->data = data;
}
EXPORT_SYMBOL(tasklet_init);


##### 6.3.3.3、调度tasklet


在驱动程序中调度tasklet可以使用`tasklet_schedule()`函数



static inline void tasklet_schedule(struct tasklet_struct *t)
{
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
__tasklet_schedule(t);
}


`test_and_set_bit()`原子地设置`tasklet_struct->state成员`为`TASKLET_STATE_SCHED`标志位,返回旧的state的值,如果返回true,那么说明这个tasklet已经在tasklet链表中,不需要再重新挂入,如果返回false,说明这个tasklet还没挂入到tasklet链表中,使用`__tasklet_schedule`函数把该tasklet挂入链表中。


##### 6.3.3.4、tasklet使用例子讲解


假设我们在硬件中断处理函数中调用`tasklet_schedule()`函数去触发tasklet来处理一些数据,比如数据复制、数据转换等。以`driver/char/snsc_event.c`为例


* 首先用request\_irq注册一个中断



//request_threaded_irq(irq, handler, NULL, flags, name, dev);
rv = request_irq(SGI_UART_VECTOR, scdrv_event_interrupt,
IRQF_SHARED, “system controller events”, event_sd);


* 进入`scdrv_event_interrupt`处理函数



static irqreturn_t scdrv_event_interrupt(int irq, void *subch_data)
{
struct subch_data_s *sd = subch_data;
unsigned long flags;
int status;

spin\_lock\_irqsave(&sd->sd_rlock, flags);
status = ia64\_sn\_irtr\_intr(sd->sd_nasid, sd->sd_subch);

if ((status > 0) && (status & SAL_IROUTER_INTR_RECV)) {
	tasklet\_schedule(&sn_sysctl_event);
}
spin\_unlock\_irqrestore(&sd->sd_rlock, flags);
return IRQ_HANDLED;

}

DECLARE_TASKLET(sn_sysctl_event, scdrv_event, 0);


分析一下上面的驱动,首先系统里面已经注册了一个中断`SGI_UART_VECTOR`,当这个硬件中断发生时,就会调用到上半部的中断处理函数,`scdrv_event_interrupt`,在该上半部的处理函数中,会调用到`tasklet_schedule`函数来执行下半部的操作,这个tasklet最终回调到的函数是`scdrv_event()`函数。


#### 6.3.4、tasklet的注意事项


**`tasklet是串行执行的。一个tasklet在tasklet_schedule()时会绑定某个CPU的tasklet_vec链表,它必须要在改CPU上执行完tasklet的回调函数才会和该CPU松绑。`** **为什么???**


我们先来看看`tasklet_action`的实现,还记得这个函数是干什么的吗?? 往上看看,我们在注册软中断的时候,会用到`open_softirq`函数来注册软中断,而这个函数的其中一个参数就是`tasklet_action`的回调,也就是当软中断执行的时候,就会调用到`tasklet_action`,我们来看看它的具体实现



0 static __latent_entropy void tasklet_action(struct softirq_action *a)
1 {
2 struct tasklet_struct *list;
3 unsigned long long ts;
4
5 local_irq_disable();
6 list = __this_cpu_read(tasklet_vec.head);
7 __this_cpu_write(tasklet_vec.head, NULL);
8 __this_cpu_write(tasklet_vec.tail, this_cpu_ptr(&tasklet_vec.head));
9 local_irq_enable();
10
11 while (list) {
12 struct tasklet_struct *t = list;
13
14 list = list->next;
15
16 if (tasklet_trylock(t)) {
17 if (!atomic_read(&t->count)) {
18 if (!test_and_clear_bit(TASKLET_STATE_SCHED,
19 &t->state))
20 BUG();
21 check_start_time(ts);
22 t->func(t->data);
23 check_process_time(“tasklet %ps”, ts, t->func);
24 tasklet_unlock(t);
25 continue;
26 }
27 tasklet_unlock(t);
28 }
29
30 local_irq_disable();
31 t->next = NULL;
32 *__this_cpu_read(tasklet_vec.tail) = t;
33 __this_cpu_write(tasklet_vec.tail, &(t->next));
34 __raise_softirq_irqoff(TASKLET_SOFTIRQ);

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值