⭐❤️万字长文带你了解Linux最核心的部分--中断❤️⭐

34 篇文章 2 订阅
19 篇文章 1 订阅


![请添加图片描述](https://img-blog.csdnimg.cn/65addb7917a34aa5bce4f707a4f71b35.webp?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA6a2U5Yqo5bGx6Zy4,size_17,color_FFFFFF,t_70,g_se,x_16)

前言

中断是指CPU在正常执行的过程中会被某些内外部事件或者程序特有的信号使得CPU暂时停止正在运行的程序,跑去执行这些让CPU发生中断的事,LINUX中可分为内部中断和外部中断。


一、Linux中断发生的条件

发生中断的情况有很多。如:

  • 按键的上升沿,下降沿触发。
  • 串口的收发完成标志位
  • 看门狗中断
  • 定时器的中断
  • CPU执行的指令有问题

    我们只用关心Linux系统自带的一些中断,所有的这些中断都会汇集到一个中断控制器,由这些中断控制器来取决于这些中断的优先级,就好比STM32单片机中NVIC控制器,也是负责决裁中断的优先级。

二、Linux内核对中断如何处理

1.中断的处理流程

  • ARM芯片对中断的处理:
  1. 设置中断源,让他可以产生中断
  2. 设置优先级
  3. 使能中断,你用哪一个中断就开启哪一个,默认中断时关闭的
  4. 产生中断:中断->中断控制器->cpu
  5. cpu每执行一条指令会检查是否有中断的产生
  6. 检查到中断就跳到相应的地址去执行,这个地址就是你要执行函数的地址
  7. 这些函数会帮你保存之前的现场以及寄存器,分辨是哪一个中断源再去处理不同的函数,执行完后在恢复现场

2.异常向量表

异常向量表就是每一条指令对应一种异常
在这里插入图片描述
比如发生了中断,cpu就会去调用下面这条语句
在这里插入图片描述
发生了复位操作cpu就会执行下面这条语句,你只用知道上面的向量异常表会对应他要执行的语句。
在这里插入图片描述
请添加图片描述

3.Linux内核对中断如何处理

3.1中断处理的核心-栈

ARM芯片属于精简指令集计算机,它对内存只有读和写的指令,所以这些数据的运算是在CPU里面进行实现的。
比如对于a=a+b这样的算式,需要经过下面4个步骤才可以实现:
在这里插入图片描述

  • 我们只用知道,cpu如果正在执行一个程序,突然有一个中断指令过来了,那么他会去执行另外一个程序,执行完那个中断程序之后才会执行之前的程序。在这个过程中我们需要保存现场和恢复现场,意思就是把CPU里面执行原本程序的寄存器存储的值保存下来,它保存在内存里面,这个内存叫栈。

  • 我们重复一下刚刚的话,假设一个函数A正在执行,突然间CPU要去执行函数B,那么会先把函数A保存在栈里面,执行完函数B后,再把函数A的数据从栈中拿出,继续执行。

  • --------------------------进程之间的切换也是如此

某一个CPU,对于多个程序,cpu是来回切换多个程序,这种切换的时间人眼是察觉不到的,这种情况成为并发。对于并发程序,也是在不断的保存栈,恢复栈的过程中,一个进程代表一个栈,。所以一般linux系统都会有默认的只能进行多少个进程,当然你也可以自己去改掉他的默认值。因为进程越多,栈的消耗就越大。在Linux中:资源分配的单位是进程,调度的单位是线程。也就是说,在一个进程里,可能有多个线程。而这些线程,之间是互相独立的,“同时运行”,也就是说:每一个线程,都有自己的栈。
在这里插入图片描述

									进程和线程的关系

3.2.中断在Linux中的演进

  • Linux系统中分为硬中断和软中断,硬中断就是不能被打断的,而且不能被嵌套中断,硬中断里面的函数越快越好,因为硬中断时间过程会拖慢整个系统的运行效率,大大降低了流畅度。
    你可以把硬中断比喻成一个数组,这个数组里面存放着很多中断。硬件中断发生后,可以去调用相应的函数去执行你去做得事
  • 软件中断
    你也可以自己去制造一个中断,这个被成为软件中断,软中断的函数也是放在一个soft irq的数组里面,可以通过flag(标志位)来判断是否发生了。在内核源码的在include/linux/interrupt.h下可以找到
    请添加图片描述
    在这里插入图片描述
    怎么触发软件中断?最核心的函数是raise_softirq,简单地理解就是设置softirq_veq[nr]的标记位。
void raise_softirq(unsigned int nr)
{
	unsigned long flags;

	local_irq_save(flags);
	raise_softirq_irqoff(nr);
	local_irq_restore(flags);
}
							    raise_softirq的函数原型

设置软件中断的处理函数使用open_softirq函数,第一个参数是哪一个软件专断,第二个是执行相应的函数

void open_softirq(int nr, void (*action)(struct softirq_action *))
{
	softirq_vec[nr].action = action;
}

软件中断可以用在中断下半部tasklet上,中断下半部是什么后面会有讲解的


在Linux使用中断只用为某个中断注册中断函数handler,我们使用的是request_irq函数:
第一个参数表示中断号,第二个表示中断后发生的函数,第三个表示用什么状态(标志位触发),第四个是名字,第五个是你的结构体

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);
}

在你写的函数中,要尽量高效简介,比如一个按键需要进行消抖,不能在handler中进行,因为会拖慢你系统的工作效率。所以Linux内核把中断函数分为上半部中断和下半部中断,上半部处理紧急的,上半部是不能被其他事件打断的,下半部处理不紧急的,中断下半部的实现主要以tasklet(小任务)和work queue(工作队列)这两种方法实现。

代码如下(示例):

data = pd.read_csv(
    'https://labfile.oss.aliyuncs.com/courses/1283/adult.data.csv')
print(data.head())

3.3使用tasklet处理下半部

我们前面介绍过软件中断(sortirq),使用tasklet处理中断的下半部
在这里插入图片描述

3.4中断上半部和下半部的关系

  1. 上半部不能被打断,执行函数要高效
  2. 下半部可以处理不那么紧急的事,是在开中断时候执行
  3. 下半部执行的时候可能会被多次打断
  4. 通过中断上半部触发中断下半部
  5. 执行中断的时候是不能休眠的

3.5中断下半部使用work_queue(工作队列)

在执行中断下半部的过程中虽然可以处理其他的中断,但是应用层的程序是执行不了的,我的理解是优先级比应用层高。如果中断下半部时间过长,那么应用层也是不工作的。所以不能使用软件中断来做,我们可以使用内核线程,线程是调度的最小单位,使得在上半部唤醒线程,使得内核线程和应用层的线程执行处在同一个竞争位置上,那么大家都有机会可以被CPU执行,可以理解为并发,这样的话就不容易出现卡顿的现象。内核线程是系统帮我们创建的,多为kworker线程。我们可以通过下面的命令来查看进程

ps -A |grep  kworker

kworker线程从工作队列中取出里面的work,并执行它里面的函数

DECLARE_WORK  (struct work_struct name,  void (*func)(void *)); 

声明了一个work之后,我们下一步要把它放进工作队列里面

schedule_work(&my_work); //添加入队列的工作完成后会自动从队列中删除

当我们把work放进工作队列后,并把kworker唤醒,当kworker抢到执行时间后,会自动调用work里面的函数。
现在我们只用把schedule_work这个函数放在中断上半部执行它就可以串联起来了。
注意:前面我们说过tasklet不能休眠,而工作队列是可以休眠的,因为是使用kworker线程,线程嘛,大家都知道是可以休眠的。

3.6 threaded irq

用request_threaded_irq申请的中断,handler不是在中断上下文里执行,而是在新创建的线程里执行,这样,该handler非常像执行workqueue,拥有所有workqueue的特性,但是省掉了创建,初始化,调度workqueue的繁多步骤。处理起来非常简单。

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)

request_irq非常类似,irq是中断号, handler是在发生中断时,首先要执行的code,非常类似于顶半,该函数最后会return IRQ_WAKE_THREAD来唤醒中断线程,

使用work来处理中断的话,一个worker线程只能由一个CPU来执行,如果是单核CPU那肯定没啥问题,不过现在大部分都是使用多核CPU了,简称SMP(是指在一个计算机上汇集了一组处理器(多CPU),各CPU之间共享内存子系统以及总线结构。它是相对非对称多处理技术而言的、应用十分广泛的并行技术)。

threaded irq为每个中断都创建了内核线程,多个kworker内核线程可以分配在cpu上执行,不让其他的cpu空着不执行工作,提高了CPU的工作效率。

四.从结构体的角度去剖析Linux中断,

前面说过,linux内核发生中断时,会跳转到一个异常向量表上,去执行相应的东西,如果把一个硬件中断比喻成一个数组,那么,每一个硬件中断还会有一个数组(其实是一个结构体),这个数组就是irq_desc数组。

4.1 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;
#ifdef CONFIG_IRQ_PREFLOW_FASTEOI
    irq_preflow_handler_t   preflow_handler;
#endif
    struct irqaction    *action;    /* IRQ action list */
    unsigned int        status_use_accessors;
    unsigned int        core_internal_state__do_not_mess_with_it;
    unsigned int        depth;      /* nested irq disables */
    unsigned int        wake_depth; /* nested wake enables */
    unsigned int        irq_count;  /* For detecting broken IRQs */
    unsigned long       last_unhandled; /* Aging timer for unhandled count */
    unsigned int        irqs_unhandled;
    atomic_t        threads_handled;
    int         threads_handled_last;
    raw_spinlock_t      lock;
    struct cpumask      *percpu_enabled;
#ifdef CONFIG_SMP
    const struct cpumask    *affinity_hint;
    struct irq_affinity_notify *affinity_notify;
#ifdef CONFIG_GENERIC_PENDING_IRQ
    cpumask_var_t       pending_mask;
#endif
#endif
    unsigned long       threads_oneshot;
    atomic_t        threads_active;
    wait_queue_head_t       wait_for_threads;
#ifdef CONFIG_PM_SLEEP
    unsigned int        nr_actions;
    unsigned int        no_suspend_depth;
    unsigned int        cond_suspend_depth;
    unsigned int        force_resume_depth;
#endif
#ifdef CONFIG_PROC_FS
    struct proc_dir_entry   *dir;
#endif
    int         parent_irq;
    struct module       *owner;
    const char      *name;
} ____cacheline_internodealigned_in_smp;

这个结构体很复杂,不过我们先关注handle_irq和一个action的链表。我们平常触发的中断最终都会汇集到GIC(中断控制器中)进行判断,然后GPC再去中断CPU。
① GIC的处理函数:
假设irq_desc[A].handle_irq是XXX_gpio_irq_handler(XXX指厂家),这个函数需要读取芯片的GPIO控制器,细分发生的是哪一个GPIO中断(假设是B),再去调用irq_desc[B]. handle_irq。
② 直接中断函数

比如GPIO模块向中断控制器发出了中断A,它对应的处理函数是irq_desc[A].handle_irq

③ 外部设备提供的处理函数
这里说的“外部设备”可能是芯片,也可能总是简单的按键。对于一个GPIO中断,可能有很多中断源,可能是按键,可能是网卡等等,每一个中断源对应了一个处理函数,这些中断源的处理函数就放在了action链表里面,当这个某个中断发生时,就会把action里面的中断源处理函数进行遍历,看看是哪一个中断源

4.2 irqaction结构体

这个结构体就是我们上面说的action链表,它里面由很多的定义。

struct irqaction {
         irq_handler_t handler;      //等于用户注册的中断处理函数,中断发生时就会运行这个中断处理函数
         unsigned long flags;         //中断标志,注册时设置,比如上升沿中断,下降沿中断等
         cpumask_t mask;           //中断掩码
         const char *name;          //中断名称,产生中断的硬件的名字
         void *dev_id;              //设备id
         struct irqaction *next;        //指向下一个成员
         int irq;                    //中断号,
         struct proc_dir_entry *dir;    //指向IRQn相关的/proc/irq/

};

当我们调用request_irq、request_threaded_irq注册中断处理函数时,内核就会构造一个irqaction结构体。在里面保存name、dev_id等,最重要的是handler、thread_fn、thread。

  • handler就是我们上面说的中断上半部函数,当中断发生时来调用这个函数
  • thread_fn对应一个内核线程thread,当handler执行完毕,Linux内核会唤醒对应的内核线程。在内核线程里,会调用thread_fn函数。这个内核线程就是我们说的当我们使用work_queue(工作队列)或者thread_irq时,就会创建一个内核线程

4.3irq_data结构体

struct irq_data { 
    u32            mask;----------TODO 
    unsigned int        irq;--------软件中断号
    unsigned long        hwirq;-------硬件中断号
    unsigned int        node;-------NUMA node index 
    unsigned int        state_use_accessors;--------底层状态,参考IRQD_xxxx 
    struct irq_chip        *chip;----------该中断描述符对应的irq chip数据结构 
    struct irq_domain    *domain;--------该中断描述符对应的irq domain数据结构 
    void            *handler_data;--------和外设specific handler相关的私有数据 
    void            *chip_data;---------和中断控制器相关的私有数据 
    struct msi_desc        *msi_desc; 
    cpumask_var_t        affinity;-------和irq affinity相关 
 	}

这个结构体是一个中转站,,里面有irq_chip指针 irq_domain指针,比如GPIO中断B是软件中断号,可以找到irq_desc[B]这个数组项;GPIO里的第x号中断,这就是hwirq。他们之间关系的建立由irq_domain这个结构体来表示,不过我们还得思考一个问题,比如gpio控制器里的第一号中断和串口中的一号中断如何区分呢,这个时候就是用到了irq_domain来表示。

4.4irq_domain结构体

函数原型:

static inline struct irq_domain *irq_domain_add_linear(struct device_node *of_node,
					 unsigned int size,
					 const struct irq_domain_ops *ops,
					 void *host_data)
{
	return __irq_domain_add(of_node_to_fwnode(of_node), size, size, 0, ops, host_data);

}

这个结构体对设备树的中断转换有很大的作用,比如在设备树中你会看到

interrupt-parent = <&gpio1>;
interrupts = <5 IRQ_TYPE_EDGE_RISING>;

它表示要使用gpio1里的第5号中断,hwirq就是5。我们会把hwirp转化为irq,irq就是软件中断号,只有我们拥有了软件中断号才可以去注册中断等等,比如这个函数request_irq(irq, handler)。在上面的设备树的属性中,gpio1会有一个irq_domain结构体,这个结构体有一个irq_domain_ops结构体,里面有一个xlate函数来解析设备树的中断属性,比如hwirq(硬件中断号)、type等,最后在通过map函数把hwirq转化为irq

4.5 irq_chip结构体

函数原型:

struct irq_chip {
    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);-----------------关闭中断
 
    void        (*irq_ack)(struct irq_data *data);---------------------应答中断
    void        (*irq_mask)(struct irq_data *data);--------------------屏蔽中断
    void        (*irq_mask_ack)(struct irq_data *data);----------------应答并屏蔽中断
    void        (*irq_unmask)(struct irq_data *data);------------------解除中断屏蔽
    void        (*irq_eoi)(struct irq_data *data);---------------------发送EOI信号,表示硬件中断处理已经完成。
 
    int        (*irq_set_affinity)(struct irq_data *data, const struct cpumask *dest, bool force);--------绑定中断到某个CPU
    int        (*irq_retrigger)(struct irq_data *data);----------------重新发送中断到CPU
    int        (*irq_set_type)(struct irq_data *data, unsigned int flow_type);----------------------------设置触发类型
    int        (*irq_set_wake)(struct irq_data *data, unsigned int on);-----------------------------------使能/关闭中断在电源管理中的唤醒功能。
 
    void        (*irq_bus_lock)(struct irq_data *data);
    void        (*irq_bus_sync_unlock)(struct irq_data *data);
 
    void        (*irq_cpu_online)(struct irq_data *data);
    void        (*irq_cpu_offline)(struct irq_data *data);
 
    void        (*irq_suspend)(struct irq_data *data);
    void        (*irq_resume)(struct irq_data *data);
    void        (*irq_pm_shutdown)(struct irq_data *data);
...
    unsigned long    flags;

我们在通过request_irq注册中断之后,系统会调用irq_chip里的函数帮我们使能了中断。
这个函数还有其他的作用,比如说执行主芯片的清理中断的操作,但对于外部设备的清中断,还是需要我们自己去完成,因为内核也不知道你用的是啥外部设备



# 总结 上面这一部分只是一些理论性的东西,如何来进行代码的编写将会在我其他的文章进行操作,如果你想更深入的了解Linux底层和应用层的知识,推荐你去这个网站学习,我的很多知识都是从B站或者韦老师的视频学的。Linux的中断是整个系统的重中之重,比如线程进程或者定时器,poll机制都是要用到中断的,你也许只会通过函数的调用去用到他们,不过你学习了Linux系统一些底层的知识将会对你的Linux应用层开发和驱动开发有极大的提高。

.韦东山老师的教程100ask.net/index

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

魔动山霸

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值