linux内核“任务”之中断

linux内核中也存在多任务,这些任务可能是不同的类型,它们之间可能也需要进行同步和互斥,在讨论linux内核中的同步和互斥之前有必要先讨论下linux内核中的“任务”类型。linux内核中的任务包括:
  1. 中断和异常
  2. 软中断和tasklet
  3. workqueue
  4. 内核定时器
  5. 内核线程
  6. 系统调用

一、中断和异常

1.中断和异常的基本概念

1. 中断:指当需要时CPU暂时停止当前程序的执行转而执行处理新情况的程序和执行过程。即在程序运行过程中,系统出现了一个必须由CPU立即处理的情况,此时,CPU暂时中止程序的执行转而处理这个新的情况的过程就叫做中断。它是计算机中最基本的一个概念,在现代计算机中毫无例外地都要采用中断技术。中断可以提高CPU的利用效率,可以提高系统的并发能力,中断也是多任务系统中调度器运行的基础。

中断最基本的场景是:计算机系统需要关心或者说知道某些事件的产生,在这些事件产生后系统会做一些处理,但是又不想一直等待并检查该事件是否已经发生了,因为如果采取等待并检查的方法则系统的处理能力就会极大的下降。(对于用户态程序来说这里还有一种处理方式,就是定时轮询,即过一段时间后查询一次事件状态。但是对于内核来说定时本身就是一种期望的事件,如果没有中断,定时本身就需要等待并检查以检查期待的时间点是否已经到达。)因而理想的方式是采用中断的方式:即系统不许要等待并检查事件的发生,而是在事件发生时,产生某种信号给CPU,以通告系统发生了某种事件,这个时候CPU再暂停止当前程序的执行转而执行处理新情况的程序。

2. 异常:在处理器执行到由于编程失误而导致的错误指令(例如除数是0)的时候,或者在执行期间出现特殊情况(例如缺页),需要靠操作系统来处理的时候,处理器就会产生一个异常。对大部分处理器体系结构来说,处理异常和处理中断的方式基本是相同的。异常与中断还是有些区别,异常的产生必须考虑与处理器时钟的同步。实际上,异常往往被称为同步中断。

异常和中断的处理方式基本是相同的,因而在不详细区分它们时可以统称为中断。有的中断是可以禁止的,有的则不可以,比如说硬件错误中断就不可禁用。内核会尽量避免禁用中断,因为这会损害系统性能。

2. IRQ

每个可以产生中断的外部设备都有一根被用作输出中断请求(IRQ)的信号线。所有IRQ线都被连接到可编程中断控制器(PIC)的输入引脚。PIC的作用:
  1. 监控IRQ信号线,检查产生的信号。
  2. 如果某跟IRQ信号线上产生了中断信号:
    • 把产生的信号转变为相应的中断向量
    • 把中断向量存放在中断控制器的I/O端口,从而使得CPU可以读取中断向量
    • 发生中断信号给处理器的INTR引脚,即产生一个中断
    • 等待CPU确认这个中断,一般是CPU往中断控制器的I/O端口写数据
  3. 返回步骤1
IRQ一般从0开始顺序编号,它和中断向量之间的映射关系是可编程的。每个IRQ都可以被单独关闭/打开。
在多处理器环境下,简单的PIC已经无法满足要求,这时候需要更复杂的中断控制器,称为高级可编程中断控制器(APIC)。在多处理器环境下,一般每个CPU都由一个本地APIC,除了本地APIC之外,还有用于将外部中断转换为本地中断的路由APIC。APIC由IRQ信号线,中断重定向表,可编程寄存器, 可以收发APIC消息的消息单元组成。重定向表中的表项可以被用来指定中断向量、优先级、目标处理器以及如何选取目标处理器,这些信息被用来将外部中断信号转变为发给一个或多个CPU的APIC的消息(最终转换为本地中断).外部中断信号向CPU中断的转换可以是静态的也可以是动态的。CPU的IRQ亲和性可以控制将中断发送到那个CPU上。(/proc/irq/n/smp_affinity)

在多处理器架构下,还有另外一种中断--处理器间中断(IPI),它用于一个处理器向另外的处理器发送中断。

因此可以这么说中断包括两种一种是IRQ类型的中断,一种是CPU自己产生的中断或者异常。

3.中断向量

CPU使用中断向量来识别中断源。CPU给每个IRQ分配了一个整数,通过这个整数CPU可以识别出不同类型的中断。中断的来源有很多,很多硬件都可以产生中断,软件本身也可以产生中断,但是CPU并不关心是谁触发了中断,它只需要知道某个中断被触发了,需要进行处理即可。对于CPU来说,中断处理的过程都是一样的:中断现行程序,转到中断服务程序处执行,回到被中断的程序继续执行。在这种设计下,当新的设备引入新类型的中断时,CPU和操作系统不用关注如何处理它。CPU只负责接收中断信号,并引用中断服务程序;而操作系统提供默认的中断服务——一般来说就是不理会这个信号,返回就可以了——并负责提供接口,让用户通过该接口注册根据设备具体功能而编制的中断服务程序。如果用户注册了对应于一个中断的服务程序,那么CPU就会在该中断到来时调用用户注册的服务程序。这样,在中断来临时系统需要如何操作硬件、如何实现硬件功能这部分工作就完全独立于CPU架构和操作系统的设计了。

而当你需要加入新设备的时候,只需要告诉操作系统该设备占用的中断号、按照操作系统要求的接口格式撰写中断服务程序,用操作系统提供的函数注册该服务程序,设备的中断就被系统支持了。中断和对中断的处理被解除了耦合。这样,无论是你在需要加入新的中断时,还是在你需要改变现有中断的服务程序时、又或是取消对某个中断支持的时候,CPU架构和操作系统都无需作改变。 

4.中断处理和中断服务程序

中断的基本处理流程:

  1. 确定与中断或异常关联的向量
  2. 寻找向量对应的处理程序
  3. 保存当前的“工作现场”(即当前执行任务的信息,内核使用的寄存器的值会被保存到pt_regs中,并在从中断处理程序返回时被恢复,pt_regs的内容依赖于平台),执行中断或异常的处理程序
  4. 处理程序执行完毕后,把控制权交还给控制单元
  5. 控制单元恢复现场,返回继续执行原程序或调度新的任务执行。在这个个点系统也会检查进程是否有信号需要处理,如果有信号需要处理,就会先处理信号。(这里的顺序是先检查是否需要重新调度,如果是就调度新的任务,否则检查是否有信号要处理并在需要的时候处理信号,最后才会考虑返回到被中断任务的中断点)
在响应一个中断的时候,内核会 执行一个函数,该函数叫做中断处理程序(interrupt handler)或中断服务程序(interrupt service routine(ISR))。产生中断的每个设备都有相应的中断处理程序。
一般来说,中断服务程序要负责与硬件进行交互,告诉该设备中断已被接收,清除中断状态以便可以继续产生中断。此外,还需要完成其他相关工作。比如说网络设备的中断服务程序除了要对硬件应答,还要把来自硬件的网络数据包拷贝到内存,对其进行处理后再交给合适的协议栈或应用程序。
由于中断服务程序的特殊性,因此对其有一些要求:
  1. 尽可能做少的事情,把不必要的事情延后做(比如接收报文的动作) ,以减少CPU在中断服务程序上花费的时间,一般最少的事情指的是可以让设备重新工作的操作。
  2. 应该支持中断嵌套,即在处理一个中断时允许新的中断发生(可以来自同一个设备也可以来自不同的设备),这样可以尽量长的时间让设备处于忙的状态,可以充分发挥设备的性能。因而中断处理程序应该被设计成可嵌套的。
  3. 某些临界区可能必须关闭中断,但是由于要求2,因而这种区域应该尽可能少

中断向量表用于存放中断或者异常向量与其中断服务程序之间的映射关系。

另外需要说明的是,中断处理可能使用内核的栈也可能使用每CPU的中断栈,具体使用那个栈需要参考实际的代码。

5.中断优先级

由于系统支持很多中断,但是这些中断的重要性、实时性或者紧急程度是不同的,因而中断之间也存在优先级的差别,在不同优先级的中断同时来临时,高优先级的中断优先得到响应。此外,如果在响应一个中断,执行中断处理的过程中,又有新的中断事件发生而发出了中断请求,应该如何处理也取决于中断事件的优先级。当新发生的中断事件的优先级高于正在处理的中断事件时,又将中止当前的中断处理程序,转去处理新发生的中断事件,处理完毕才返回原来的中断处理。

6.中断屏蔽

中断屏蔽也是一个十分重要的功能,所谓中断屏蔽是指通过设置相应的中断屏蔽位,禁止响应某个中断。这样作的目的,是保证在执行一些重要的程序中不响应中断,以免造成迟缓而引起错误。例如,在系统启动执行初始化程序时,就屏蔽键盘中断,使初始化程序能够顺利进行。这时,敲任何键,都不会响应。当然对于一些重要的中断是不能屏蔽的,例如重新启动、电源故障、内存出错、总线出错等影响整个系统工作的中断是不能屏蔽的。因此,从中断是否可以被屏蔽来看,可分为可屏蔽中断和不可屏蔽中断两类。

7.中断共享

每个中断都应该对应于一个中断向量,但是系统支持的中断向量数目受限于硬件资源,是有限的。但是外设却越来越多,因而可能有多个设备需要使用相同的中断向量,这称为中断共享。中断共享需要内核和硬件的支持。

8.中断数据结构

中断涉及到很多数据结构,这里列出几个比较重要的数据结构,如果想看数据结构完整的定义可以去看内核代码。

1. struct irq_desc

该结构是一个中断描述符。包含了irq_data, irqaction链表,irq状态,irq_flow_handler_t等等信息。其中irq_flow_handler_t是中断向量表中对应于irq号的handler,而irqaction是保存在irq_desc中的,由irq_flow_handler_t来调用。irq_flow_handler_t负责处理底层的细节,比如中断确认,边沿触发以及电平触发的处理。

2. struct irq_desc irq_desc[NR_IRQS];

包含了 NR_IRQS个中断描述符的数组,在中断未注册时,数组元素会被默认初始化为关闭中断,中断处理函数为handle_bad_irq...。在真正初始化时,会提供它对应的irq_flow_handler_t。注意irq_set_handler用来设置该irq线的irq_flow_handler_t,而setup_irq则是往irq_desc中添加一个irqaction。

3. struct irq_data

存储了会传递给芯片相关的处理函数的irq相关数据以及和irq相关的芯片信息(包含了一个struct irq_chip结构指针)。

4. struct irqaction

中断服务函数描述符,存储了中断服务函数相关的信息。

5.struct irq_chip

该数据结构存储了最低层的和中断相关的芯片等级的函数指针(包括了初始化、使能、关闭、确认、中断结束(eoi)、设置中断亲和性等函数指针)。是一个芯片等级的硬件中断描述符。该结构考虑到了可能遇到的所有IRQ的特性,因而对于一个特定的IRQ来说,它可能只实现了其中的一部分操作。它表示一个IRQ芯片,它提供的都是芯片级的操作处理函数。

9. low level API

系统包含了NR_IRQS个中断描述符的数组,在early_irq_init中,数组元素会被默认初始化为关闭中断,中断处理函数为handle_bad_irq,irq_chip会被设置为no_irq_chip。中断框架提供了几个low level的API用于处理和中断控制器相关的事情:
  1. int irq_set_chip(unsigned int irq, struct irq_chip *chip):用于设计irq对应的irq chip。
  2. int irq_set_handler_data(unsigned int irq, void *data)和irq_set_chip_data(unsigned int irq, void *data):设置irq chip的处理函数所需要的data
  3. irq_set_irq_type(unsigned int irq, unsigned int type):设置irq对中的中断的触发机制
  4. void irq_set_chip_and_handler(unsigned int irq, struct irq_chip *chip, irq_flow_handler_t handle):设置irq对应的irq chip以及中断处理函数,它设置的是irq_desc中的handle_irq,而不是irqaction类型的action。这二者的区别在于前者一般使用的是一个系统已经定义好的处理函数,比如handle_level_irq、handle_percpu_irq、handle_fasteoi_irq、handle_edge_irq,它需要调用irq chip中的函数完成芯片级的中断处理,比如确认中断,在中断处理完后做EOI,同时这些处理函数还需要做的一个重要的工作就是调用与该irq关联的irqaction类型的action;irqaction类型的中断处理函数是完成中断处理的函数,即对中断进行响应的函数,它是通过requst_irq注册到irq对应的irq_desc中从而和irq关联起来的。
  5. void irq_set_handler(unsigned int irq, irq_flow_handler_t handle):该函数用来设置irq对应的中断处理函数,注意它设置的handler也是irq_desc中的handle_irq域
  6. void irq_set_chained_handler(unsigned int irq, irq_flow_handler_t handle):该函数用来设置irq对应的中断处理函数,注意它设置的handler也是irq_desc中的handle_irq域,它的特殊之处在于,用它设置完一个irq后,该irq会被标记为IRQ_NOREQUEST, IRQ_NOPROBE, and IRQ_NOTHREAD,也就是说该irq变为不可request,不可probe,不可线程化。
系统还提供了其它的一些API,详细的可以参考irq.h头文件。

10. IRQ注册和释放

如果要使用IRQ类型的中断,必须先向系统注册;然后再不需要使用时在解除自己的注册。这是由以下两个函数完成的。

int request_irq(unsigned int irq,irqreturn_t (*handler)(int, void *, struct pt_regs *),unsigned long flags,const char *dev_name,void *dev_id);//0表示成功,或者负的错误码

void free_irq(unsigned int irq, void *dev_id);
参数及其含义如下:
  •  irq: 请求的中断线,即IRQ号
  •  handler: 中断服务程序. 
  •  flags:一个与中断管理相关的选项的位掩码
  •  dev_name:设备名字,用在 /proc/interrupts 来显示中断的拥有者
  •  dev_id:传回给handler的信息
flags 中可以设置的位如下:
  • SA_INTERRUPT:当该位被设置时,表示这是一个"快速"中断处理。快速处理在当前处理器上禁止中断来执行。当该标志被设置时,在handler被执行时,当前处理器上的其他所有中断都被禁止。大多数中断都不设置该标志,常见的设置了该标志的中断是定时器中断。通常来说,不设置这个标志意味着中断可以分两部分处理,一部分完成极少必要的工作使得硬件可以重新工作,而另一部分处理在通过其它机制来完成,比如tasklet或workqueue。
  • SA_SHIRQ:这个位表示中断可以在设备间共享。设置了该标志时,dev_id必须被指定。内核维护了一个与中断相关联的共享处理者列表,dev_id 可认为是区别它们的签名,系统会按照其注册的顺序将其添加到该共享者链表中,即每次都添加到尾部。如果2个驱动要在同一个中断上注册 NULL作为它们的签名,在卸载时就可能出问题在。 当请求一个共享的中断, 如果下列条件之一为真,则request_irq 成功:
    • 中断线空闲.
    • 所有这条线的已经注册的处理者也指定共享这个 IRQ.

无论何时如果有2个或多个驱动共享了同一个中断,并且硬件在这条中断线上产生了中断,,则内核为这个中断调用每个注册的处理者(按照它们注册的顺序),并将它们注册时提供的dev_id传回给其对应的中断服务程序。 因此, 一个共享的处理者必须能够识别它自己的中断并且当它自己的设备没有被中断时应当快速退出,如果是自己的中断并且已经处理完成则返回IRQ_HANDLED,如果不是自己的中断则返回IRQ_NONE。在使用这种类型的中断时,由于中断信号线不是独享的,因而驱动不应该调用enable_irq和disable_irq。

  • SA_SAMPLE_RANDOM:这个位表示产生的中断能够有贡献给 /dev/random 和 /dev/urandom 使用的entropy pool. 这些设备在读取时返回真正的随机数,因而可以帮助应用程序软件为加密选择安全密钥. 这样的随机数从一个由各种随机事件贡献的entropy pool中提取. 关于该标志有三个原则:
    • 如果该设备产生中断的时机是随机的则应当设置这个标志. 
    • 但是如果中断是可预测的( 例如, 一个帧抓取器的场消隐),这个标志不值得设置 -- 它无论如何不会对系统加密有贡献. 
    • 可能被攻击者影响的设备不应当设置这个标志; 
由于中断资源的稀缺性,因而应该在当设备第一次打开时, 在硬件被指示来产生中断前调用 request_irq。应该在设备最后一次被关闭时, 在硬件被告知不要再中断处理器之后调用free_irq。这样做的的缺点是你需要保持一个每设备的打开计数,以便于你知道什么时候中断可以被禁止。但是这个代价是值得的,因为如果在初始化时就注册了中断,但是又长期不使用,就会浪费及其宝贵的中断资源。

该函数首先根据irq号找到对应的irq_desc,然后进行初始化,并且添加action到irq_desc的action链表中。

删除中断相对简单,系统使用irq号和dev_id找到相应的中断并将其从系统中删除。

注意这里说的是IRQ的注册和删除,而不是所有中断的注册和删除,因为处理器本身产生的中断编号是预先可知的,这类中断在系统初始化时完成其分配和初始化,在系统运行期间不会改变。

/proc/interrupts 的显示展示了有多少中断硬件递交给系统中的每个 CPU。

/proc/stat中intr开始的行显示了中断的信息.第一个数是所有中断的总数,而其他每一个代表一个单个IRQ线,从中断 0 开始. 所有的计数跨系统中所有处理器而汇总的。

11.探测可用中断号

对于外部设备来说,如果要使用中断就要向系统进行注册,在注册时,很重要的一个参数是IRQ号,知道该号是很重要的,但是分配给某种设备的IRQ号并不一定是固定的(尤其是在不同的平台上),因而就需要探测哪个中断是分配给该设备的。下列函数用于辅助完成这个动作:

unsigned long probe_irq_on(void);

这个函数返回一个未使用的中断的位掩码。驱动必须保留返回的位掩码,并且在后面传递给 probe_irq_off。在这个调用之后,驱动应当安排它的设备产生至少一次中断
int probe_irq_off(unsigned long);
在驱动驱动设备产生一个中断后,驱动调用这个函数,并且使用之前由probe_irq_on 返回的位掩码作为参数。probe_irq_off 返回在"probe_on"之后发出的中断号。如果没有中断发生,返回 0 (因此, IRQ 0 不能被探测)。如果多于一个中断发生,probe_irq_off 返回一个负值。
在进行探测时,在调用了probe_irq_on后,应该启用中断,然后在驱动产生一个中断之后调用probe_irq_off之前应该禁用中断。另外在probe_irq_off之后,要处理设备上待处理的中断。
需要注意的是自动探测对于module来说是不适用的,因为在使用module时无法限制只有一个调用者进行了探测。
也可以不使用这些API而完全由程序自动探测中断号,原理是一致的,启用所有未被使用的中断,产生一个中断,然后检查那个中断号上产生了中断。

12.启用和禁止中断

有时设备驱动必须在一段时间(尽可能短)内阻塞中断的递交(使用"自旋锁"是一种典型的场景)。通常情况下,当持有自旋锁时必须阻塞中断。在涉及自旋锁时有多种方式来禁止中断。但是作为一个原则,应该尽量少禁止中断,即使在设备驱动程序中也是如此,同时禁止中断这种技术手段不应该在驱动程序中作为互斥机制来使用。

1 禁止单个中断

void disable_irq(int irq);
void disable_irq_nosync(int irq);
void enable_irq(int irq);
这些函数用于更新在可编程控制器中的指定的中断的掩码,因而就能在所有处理器上禁止或者启用指定的中断。这些函数能够嵌套调用,如果disable_irq 被连续调用2次,则如果要重新时能中断,则需要2个enable_irq调用。
disable_irq不仅禁止给定的中断,还等待一个当前执行的中断处理结束。如果调用disable_irq的任务持有中断处理需要的任何资源(例如自旋锁),系统可能死锁。disable_irq_nosync 与 disable_irq 不同,它立刻返回。因此使用disable_irq_nosync快一点,但是它可能使你的设备处于竞态状态。

2 禁用所有中断

void local_irq_save(unsigned long flags);
void local_irq_disable(void);
这两个函数禁用本地处理器上的中断。local_irq_save把当前中断状态保存到flags中,然后在当前处理器上禁止中断递交。local_irq_disable只关闭本地中断递交而不保存状态,因而只有当你可以确信没有其它地方禁止了中断时才能使用该函数。
void local_irq_restore(unsigned long flags);
void local_irq_enable(void);
local_irq_restore会恢复由local_irq_save存储于flags的状态, 而 local_irq_enable 无条件打开中断。 不象disable_irq, local_irq_disable不跟踪多次调用。如果调用链中有多于一个函数可能需要禁止中断, 应该使用 local_irq_save。
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值