Linux中断和中断处理程序

Linux中断和中断处理程序

由于处理器的速度与外设的速度相差很大,无法采取处理器向外设发出请求然后等待的方法。处理器与外设通信的方法:

  • 轮询:处理器定期对设备的状态进行查询(缺点:在不需要通信的情况下,处理器仍然需要周期性地重复执行)
  • 中断:一种特殊的电信号,由硬件设备发送给处理器。处理器接收信号后,由操作系统负责对数据进行处理

中断随时可以产生,内核随时可能因为新到来的中断被打断。

从物理层面讲,中断是一种电信号。

  • 由硬件设备产生,直接送入中断控制器的输入引脚中。中断控制器是个简单的电子芯片,将多路中断管线采用复用技术只通过一个和处理器相连接的管线与处理器通信。
  • 在接收到一个中断后,中断控制器会给处理器发送一个电信号。
  • 处理器检测到信号后会中断当前工作处理中断。接着,处理器通知操作系统中断产生。
  • 操作系统对中断进行处理。

不同的设备对应的中断不同,每个中断通过一个唯一的数字标志,使得操作系统能够对中断进行区分。

中断值被称为中断请求(IRQ)线,每个IRQ线被关联一个数值量。

异常:在产生时必须考虑与处理器时钟同步,常常被称为同步中断。当处理器在执行时出现错误或者特殊情况(缺页)必须依靠内核来处理的时候,处理器会产生一个异常。

在响应特定中断的时候,内核会执行一个函数,叫做中断处理程序或者中断服务例程。产生中断的每个设备都有一个相应的中断处理程序。一个设备的中断处理程序是设备驱动程序的一部分(在driver中实现中断处理程序)。设备驱动程序时用于对设备进行管理的内核代码。

中断处理程序与其他内核函数的真正区别在于中断处理程序是被内核调用来响应终端的,运行在中断上下文(也被称为原子上下文)中。

中断程序随时可能发生,需要保证中断处理程序快速执行,以便恢复中断代码的执行;同时,中断处理程序的工作量可能会很大。由于这两种目的相排斥,所以把中断处理分为两个部分。

  • 中断处理程序是上半部(top half),接收到一个中断就立刻开始执行,但只做有严格时限的工作,例如对接收的中断进行应答或者复位硬件。
  • 能够允许推迟的工作都会在下半部(bottom half)完成。

以网卡接收数据为例,将数据拷贝到内存的整个过程是在上半部实现的,对数据包的处理是在下半部实现的。

上半部

注册中断处理程序

如果设备使用中断,那么对用的驱动程序需要注册一个中断处理函数。中断函数的注册可以通过request_irq()实现。

static inline int __must_check 
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
	    const char *name, void *dev)

__must_check是指调用函数必须处理函数的返回值。

  • 第一个参数irq是要分配的中断号,对于一些设备,该值是预先确定的。对于其他设备,该值可以通过探测获取或者通过编程动态确定。

  • 第二个参数handler是一个指针,指向这个中单的实际中断处理程序。当操作系统接收到中断该函数被调用。

    • typedef irqreturn_t (*irq_handler_t)(int, void *);
      
  • 第三个参数flags是中断处理程序标志,值可能一下以下一个或多个:

    • IRQF_DISABLED:内核在处理中断处理程序期间,禁止所有的其他中断(一般不会进行设置,主要对象是希望快速执行的轻量级中断)。
    • IRQF_SAMPLE_RANDOM:表示该设备产生的中断对内核熵池(entropy pool)有贡献。内核熵池负责提供从各个随机事件中导出的真正的随机数。如果设置该标志,则来自该设备的中断间隔时间会作为墒填充到熵池。如果设备产生中断的速率是不可预知的,则可以进行设置。
    • IRQF_TIMER:为系统定时器的中断处理特别准备。
    • IRQF_SHARED:表示可以在多个中断处理程序之间共享中断线。如果不指定,则每条中断线只能有一个处理程序。
  • 第四个参数是与中断相关的设备的ASCII文本表示。

  • 第五个参数dev用于共享中断线。当一个中断处理程序需要释放时,dev提供唯一的标志信息,以便在共享中断线的多个中断处理程序时删除指定的程序。如果无需共享中断线,则该参数可为NULL。

中断注册函数request_irq()成功执行会返回0,非0值表示有错误产生,此时,中断处理程序注册失败。

request_irq()函数可能会睡眠,因此不能在中断上下文或其他不允许阻塞的代码中调用该函数。该函数可能会睡眠的原因是,在注册的过程中,内核需要在/proc/irq文件中创建一个与中断对应的项。函数proc_mkdir()用来创建该procfs项,proc_mkdir()通过proc_create()对procfs项进行设置,而proc_create()会调用kmalloc()请求分配内存,而kmalloc()是可以睡眠的

释放中断处理程序

卸载驱动程序时需要注销相应的中断处理程序并释放中断线,需要通过调用函数实现:

const void *free_irq(unsigned int irq, void *dev_id);

如果指定的中断线是不共享的,那么该函数删除处理程序的同时将禁用这条中断线;如果中断线是共享的,那么仅删除dev对应的中断处理程序。中断线将在所有的处理程序删除完后才被禁用。

dev是在共享的中断线上确定要删除的中断处理程序的唯一标志。如果dev非空,那么无论中断线是否共享,都必须与需要删除的处理程序相匹配。必须在进程上下文中调用free_irq()。

中断处理程序
static irqreturn_t xxx_handler(int irq, void *dev);

类型与request_irq()中的参数类型相匹配。irq是处理程序要响应的终端号,第二个参数dev是通用指针,与request_irq()的dev必须一致,可以用于区分共享同一中断处理程序的多个设备。

中断处理程序的返回值是一个特殊类型irqreturn_t。中断处理程序可能返回两个特殊的值:

  • IRQ_NONE:中断处理程序检测到一个中断但对应的设备并不是在注册函数时指定的产生源,产生该值
  • IRQ_HANDLED:中断处理程序被正确调用并且是对应的设备产生的中断,产生该值

通过返回值,内核可以知道设备产生的中断是否已被请求处理。

重入和中断处理程序

Linux的中断处理程序是无需重入的,当一个给定的中断处理程序在执行时,相应的中断线在所有处理器上被屏蔽,防止在同一中断线上接收另一个新的中断。不同中断线的中断可以被处理。

共享的中断处理程序

共享的处理程序与非共享的处理程序的差异:

  • request_irq()的参数flags必须设置IRQF_SHARED标志。
  • 对每个注册的中断处理程序,dev参数必须唯一。指向任一设备结构的指针可以满足该要求。不能给共享的处理程序传递NULL值。
  • 中断处理程序必须能区分它的设备是否真的产生了中断,这需要硬件的支持与处理程序中相关的处理逻辑。

内核接收一个中断后,将依次调用在中断线上注册的每一个处理程序。如果处理程序确定相对应的设备没有产生设备,那么处理程序立刻退出。这需要硬件设备提供状态寄存器,以便中断处理程序进行检查。

中断上下文

当执行一个中断处理程序时,内核处于中断上下文(interrput context)。

进程上下文是一种内核所处的操作模式,内核代表进程执行系统调用或者运行内核线程。在进程上下文中,可以通过CURRENT关联当前进程。由于进程是以进程上下文的形式连接到内核中的,因此进程上下文可以睡眠也可以调用调度程序(是指进程可以睡眠也可以调用程序吗??)。

因为没有后备进程,中断上下文不可以睡眠,不能调用函数。如果一个函数睡眠,就不能在中断处理程序使用,这是对中断处理程序使用函数的限制(为什么中断不能睡眠)。

中断上下文有严格的时间限制,因为它打断了其他代码。中断上下文中的代码应该迅速简介,尽量不要处理繁重的工作。因为这种异步执行的特性,所以应尽量把工作从中断处理程序中分离,放在下半部在更合适的时间执行。

中断程序处理栈可以共享所中断进程的内核栈,在32位系统上栈的大小是8KB;也可以每个中断处理程序独占一页,在32位系统上时4KB。尽量节约内核栈空间

中断处理机制的实现

过程:

  • 设备产生中断,通过总线把电信号发送给中断控制器
  • 中断控制器在激活的情况下(允许被屏蔽)会把中断发往处理器
  • 处理器停止当前任务,关闭中断系统,跳转到内存预定义位置开始执行代码(预定义位置由内核设置,是中断处理程序的入口点)。对于每条中断线,处理器会跳到对应的位置,获取接收中断的IRQ
  • 内核调用函数do_IRQ()
  • do_IRQ()需要确保在该中断线上有一个有效的处理程序,且程序已启动但尚未被执行。在这种情况下,do_IRQ()会调用handle_IRQ_event()运行中断线的中断处理程序。
  • 函数执行完后,返回到do_IRQ()。该函数做清理工作返回到初始入口点,再从入口点跳转到函数ret_frm_intr(),该函数检查重新调度是否正在挂起。
    • 如果重新调度正在挂起
      • 如果内核返回用户空间(用户进程中断),schedule()将被调用
      • 如果内核返回内核空间(内核本身被中断),只有在preempt_count为0时,schedule()才会被调用
    • 如果没有挂起的工作,那么寄存器恢复,内核恢复到中断的点。
/proc/interrupts

存放的是系统中与中断相关的统计信息。

中断控制
对整个处理器所有中断的处理

禁止与激活当前处理器的中断:

local_irq_disable();//cli
local_irq_enable(); //sti

这两个函数一般以单个汇编指令实现。这两个函数可能会产生风险,所以需要一种机制把中断恢复到以前的状态而不是简单的禁止或激活。

unsigned long flags;	//flags通过值传递
local_irq_save(flags);
local_irq_restore(flags);

对这两个函数的调用必须在同一个函数中进行。

而以上几个函数既可以在中断中调用,也可以在进程上下文中调用。

cli():禁止系统中所有处理器上的中断

sti():激活所有处理器的中断

现在已删除这两个函数,如果想保证对共享数据的互斥访问,必须结合自旋锁等方法。

取消全局cli()的优点:

  • 强制驱动程序编写者实现真正的加锁。因为特定目的的细粒度加锁比全局锁更快
  • 使得代码更具流线型,避免了代码的成簇布局,中断系统更简单也更易于理解。
对系统中某条中断线的处理
void disable_irq(unsigned int irq);
void disable_irq_nosync(unsigned int irq);
void enable_irq(unsigned int irq);
void synchronize_irq(unsigned int irq);

前两个函数禁止中断控制器指定的中断线,即禁止给定中断向系统中所有处理器的传递。并且disable_irq()只有在当前正在执行的处理程序完成后才能返回。因此调用者不仅要确保不在指定线上传递新的中断,还要确保所有已经开始执行的处理程序已全部退出。而disable_irq_nosync()不会等待当前中断处理程序执行完毕。

后两个函数启用中断控制器指定的中断线。synchronize_irq()等待一个特定的中断处理程序的退出。如果该处理程序正在执行,那么该函数必须退出后才能返回。

这些函数的调用可以嵌套,但是对于disable_irq()、disable_irq_nosync()函数,每调用一次都需要相对应的调用一次enable_irq()。当enable_irq()完成最后一次调用,才能真正激活中断线。这三个函数可以从中断或进程上下文中调用,而且不会睡眠。

中断系统的状态

了解中断系统的状态,例如中断是禁止的还是激活的

irqs_disable()

如果本地处理器中的中断系统被禁止,则返回非0,否则返回0。

检查内核当前上下文的接口:

in_interrupt() //如果内核当前处于任何类型的中断处理中,返回非0  说明内核此刻正在执行中断处理程序或者下半部处理程序
in_irq()//在内核正在执行中断处理程序时返回非0

如果in_interrupt()返回0,说明内核处于进程上下文中。

中断控制方法的列表

小结

中断是一种由设备使用的硬件资源异步向处理器发信号,由硬件打断操作系统。

大多数硬件通过中断与操作系统通信。对给定硬件进行管理的驱动程序注册中断处理程序是为了响应并处理来自相关硬件的中断。中断过程所做的工作包括应答并重新设置硬件。从设备拷贝数据到内存以及反之,处理硬件请求,并发送新的硬件请求。

内核提供的接口包括注册和注销中断处理程序、禁止中断、屏蔽中断线以及检查中断系统的状态。

因为中断打断了其他代码的执行。所以必须赶快执行完,但同时中断有大量的工作要做,所以为了获取平衡,内核把中断的工作分为两半。中断处理程序是上半段,下半段将在后面讨论

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值