驱动学习之中断

前言:

我们知道外部设备与处理器交互的一般手段有两种–中断与轮询。但是由于轮询是比较消耗CPU资源的,所以现代设备大多数都采用中断与处理器通信。处理器在中断到达时候会根据中断号找到对应的处理函数对该信号进行处理。这些处理函数我们成为中断处理例程,设备驱动程序负责为设备提供相应的中断处理例程,并注册到系统。从设备发出中断信号,到处理器最终调用处理例程处理完成。期间会经过很多步骤,这个过程构成了中断处理框架,本文将描述linux系统下的中断处理设计框架,然后讨论驱动程序如何利用接口注册中断处理例程。

1.1 中断硬件架构

我们都知道你,处理器对于中断一般只有两个引脚,所以为了管理众多的外设,出现了中断控制器(pic),下面是一个典型的中断控制器与cpu跟外设组成的结构:

在这里插入图片描述

1.2 pic与软件中断号

在实际的使用中,设备初始化的时候都会对pic进行相应的初始化配置,这里我们就不分析这部分内容了。我们这里需要理解的是软件终端号irq,它是发生中断时cpu从pic中读取到的中断号,与我们在芯片手册中看到中断号是不一样的。在操作系统对应的中断处理框架中,中断号是很重要的,处理器会根据中断号来查找对应的中断处理例程。

1.3 中断向量表

中断向量表的每一项都是一个中断或者异常的入口地址,这个我们在学习uboot的时候已经知道。外部设备产生的中断通常对应一个特定的项,这个是通用的外部中断处理函数的入口,在进入通用的函数入口之后,我们就需要知道是哪个外设产生的中断了,这就需要用到中断号了。中断向量表是由操作系统在初始化阶段来填写的,对于外部中断,操作系统负责实现一个通用的外部中断处理函数,然后把改函数放到向量表对应的位置。

1.4 通用的处理函数

从上面我们知道,操作系统会有一个外部中断通用的处理函数,但这还不是中断发生一开始执行的东西,在这之前会有处理器硬件逻辑实现的一些特定的动作,比如保存当前任务的上下文,屏蔽中断等,做了这些特定的操作之后,才会从中断向量表中找到通用的处理函数。不同架构的通用处理函数也不尽相同,但是大多都会是先从pic读取软件终端号irq,这部分一般会用汇编实现,然后调用一个C函数,大部分平台的这个C函数都是do_IRQ,但是也有例外比如ARM平台是asm_do_IRQ。
中断程序大部分的精华都在这个do_IRQ中,该函数返回之后,通用处理函数也就剩下恢复现场的工作。这也标志着中断处理的结束。
通常情况下处理中断的时候处理器会自动关闭掉处理外部中断的能力,因此如果我们不主动打开中断的话,整个处理过程都是中断关闭状态的,所以如果我们驱动程序中实现的中断处理例程时间比较长的话,则会导致系统很长时间不能接受中断,这将会导致某些数据的丢失或操作系统响应时间边长。为了解决这一问题,linux内核为驱动程序提供的中断处理机制分两个部分:HARDIQR和SOFTIRQ,前者就是所谓的顶半部,后者就是底半部,SOFTIQR会在中断开启的情况下执行,这时候设备可以继续打断处理器。在do_IRQ函数中,对irq_entry的调用可以认为是HARDIRQ的开始,而SOFTIRQ则在irq_exit中完成。

1.5 do_irq

上一节介绍到do_iqr将处理中断的大部分内容,虽然各个平台的实现代码不尽相同,但是原理基本一样,我们这里分析一个典型的实现:

在这里插入图片描述
这个函数看似比较简单,其实做了大量的工作,我们这里仔细的分析一下:

  1. 首先来看参数,一个是通用处理函数从pic读取到的软件中断号,一个是保存下来的被中断任务的执行现场。
  2. 首先会把regs赋值给一个_irq_regs变量,然后会把旧的_irq_regs保存到old_regs,这样中断处理过程中,每个cpu都可以访问中断现场。
  3. irq_entry会告诉系统,现在进入了中断处理的上半部分。
  4. check_stack_overflow会检查当前中断是否会导致栈溢出。在中断嵌套比较多的情况下,需要保护的现场过多,就会出现栈溢出,如果有溢出风险,就会打印出当前栈信息,如果系统有看门狗,可能会reset
  5. do_irq的核心是generic_hadnle_irq,该函数会通过软件中断号在irq_desc数组中找到对应的项,然后执行对应的handle函数。这里我们就有疑问,irq_desc这个数组是哪来的?怎么回找到对应的处理函数呢?
    在这里插入图片描述

1.6 generic_hadnle_irq

首先分析第一个问题,irq_desc数组怎么来的,这个数组其实是系统初始化的时候,根据平台不同,定义一个大小不同的数组,然后初始化数组成员中某些项,这里就有一项irq,及headle-irq,所以generic_hadnle_irq执行的时候会根据软终端号来找到对应项,然后调用headle-irq,但是headle-irq干了什么呢?为什么就会调用到我们注册的中断处理例程呢?
这是因为headle-irq函数会调用action->handler,这个函数是我们使用requset_irq注册的时候,会把我们中断处理例程添加到action的handler位置。所以这里就根据中断号找到了要处理的中断例程。还是很简单的,下面列出irq_desc的数组。供大家学习

在这里插入图片描述

这里面最重要的结构体我认为是struct irqaction
在这里插入图片描述

  1. irq ----软件中断号
  2. dev_id----这个是我们申请注册一个中断调用requser_irq函数的最后一个参数,一般这个参数可以添null,但是如果是多个设备共享一个中断号的时候,就不能添null,最好添这个设备的结构体。
  3. next也是指向下一个action,只有在多个设备共享一个中断号的时候才会用到,因为如果一条中断线上只有一个设备的话,就不存在链表了
  4. dir是用来创建在proc文件系统中的目录项。
  5. irq_thread没研究呢,应该是注册时候函数用request_threaded_irq的时候回用到。

下图是中断处理的整个过程,分为两类,分别是HARDIR与SOFTIRQ,前者一般是处理器屏蔽外部在中断的时候工作,而后者会在工作前启动处理器处理外部中断的能力。大致的处理流程就是中断到来之后硬件逻辑会保存现场,然后调用do_irq函数,根据中断号找到irq_desc项,然后调用handler_irq 进而调用action中的handler函数。函数退出就是HARDIR处理完成,然后中断进入SOFTIRQ部分。

在这里插入图片描述

1.7 注册过程

前面之所以用了很多篇幅讲解架构,是因为想让大家对中断函数的注册及处理有个大致的认识,这里说一下liunx中断框架如何与驱动交互,向action中安装自己的处理例程。
request_irq()
上图是注册中断的接口,我们这里简单描述一下:

  1. irq就是软件中断号
  2. irq_handler 就是我们之前提到的中断处理例程,也是核心
  3. flags会影响到内核安装irq_handler的行为,如果是中断共享的情况,必须使用IRQF_SHARED,因为之后会检查。
  4. name就是当前的设备名称
  5. dev会作为指针传入中断处理例程。我么你可以它来传递参数。在中断共享的情况下,不能为空,一般传入设备驱动对应的结构体。但是我感觉最好的用处就是传参。
  6. 有上面可知,函数会直接调用request_threaded_irq(),会多一个参数NULL,该参数是irq_thread

request_threaded_irq函数注册的过程还是很长的。这里就不分析细节了,只要明白起作用就是将中断处理例程添加到对用的action中。期间可能会遇到中断共享的情况,发现对用的action中的handler已经有了函数。
注册安装完之后,会在/proc/irq/xxx中断号/ 的路径下生成name对应的文件。

1.8 irq_pthread机制

因为到现在为止,没接触过irq_pthread机制,所以不是很懂,大概了解了一下,用request_threaded_irq函数来安装一个中断的时候,要先实现thread_fn成员,然后request_threaded_irq会创建一个内核线程irq_thread,然后该线程会被睡眠等待中断发生来唤醒。当中断发生时,action->handler只负责唤醒睡眠的irq_thread,后者将调用thread_fn进行实际的中断处理。因为irq_thread实质上是系统的一个独立的进程,所以采用这种方式将使实质的中断处理发生在进程上下文中。

1.9 free_irq

free_irq做的东西几乎与request_irq完全相反,它会通过irq找到数组中的对应项,然后根据dev_id遍历找到对应的action。如果只有一个,那么就直接释放,同时还会屏蔽掉PIC中该条线的中断,并且会删除proc文件系统下的节点。

1.10 SOFTIRQ

前面所有讨论的都是HARDIRQ的处理流程,下面再来看看linux内核是怎么实现SOFTIRQ,该功能在do_irq中是由irq_ext实现。
在这里插入图片描述
第一步会设置一个标识,来表示HARDIRQ中断上下文结束,在配置了内核可抢占的系统中,也开启了内核抢占。
第二步调用invoke_softirq是真正处理SOFTIRQ部分的函数。但是有两个前提,就是当前不在中断上下文且cpu的副本结构中irq_cpustat_t
中的_softing_pending中有等待softirq。这个实现的机制这里不讨论了,接着分析invoke_softirq
这里先介绍一个枚举,这是系统定义的所有softirq的类型:
在这里插入图片描述
上面的每个softirq对应_softing_pending中的一位,内核在此基础上实现了一个softirq_vec,用来放置对用的处理函数。所以_softing_pending的处理核心就是从第到高扫描_softing_pending,发现某一位置1,则调用softirq_vec对应的处理函数,这个过程会一直延续下去直到_softing_pending为0;
有时候会出现一个中断太长时间处理softirq部分,导致中断流程迟迟无法结束,因此linux系统在初始化期间生成了一个新的进程ksoftirqd,改进程运行时要完成的主要任务就是调用do_softirq来执行等待中的softirq,如果没有softirq需要处理,则进程将进入睡眠。所以如果处理时间太长,invoke_softirq就会唤醒该进程。让调度器来平衡当前中断在SOFTIRQ部分的工作负荷。

1.11 中断处理例程

这里中断例程是自己编写,这里直说几个注意事项:

  1. 首先中断处理例程是在中断上下文执行,这对它的实现提出了某些限制,我们的确保中断上下玩中不会出现进程调用,确保他们不会进入睡眠。
  2. 参数:函数包括两个参数,一个是中断号,一个是我们申请中断时传入的dev_id
  3. 返回值:有三种返回值:IRQ_NONE、IRQ_HANDLED、IRQ_WAKE_THREAD ,分别对应不同的情况,IRQ_NONE对应的是该中断处理的不是自己的中断,意思就是函数中可以通过读设备中断状态,看一下是否是我们对应的中断产生了。如果不是则返回IRQ_NONE,这在中断共享的时候是必须判断的;IRQ_HANDLED是函数正常的返回;IRQ_WAKE_THREAD是中断处理例程被用来作为唤醒一个等待在他的irq上的一个进程使用时,返回该值;
  4. 另外,由于不能有睡眠等操作,中断函数中访问共享资源的方法就派出了信号量与互斥锁,绝大多是会采用自旋锁。

1.12 中断共享

这里为什么再次提到中断共享,是因为有个问题没说,对于中断共享,我们申请中断的时候,flags参数必须为IRQF_SHARED,同时应该提供唯一的dev_id,因为在free_irq的时候,会根据dev_id来释放irq线上对应的action。而且当中断共享的时候,只要这条中短线上有中断出发,所有的actiond中的handler都会执行,所以我们应该读我们设备的中断寄存器来判断是否是我们设备的中断,如果不是,则立即返回IRQ_NONE。

  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值