05中断

1、中断概述

中断机制的应用场景:让处理器和外部设备能协同工作,且不会降低机器的整体性能。

中断过程:当接收到一个中断后,中断控制器会给处理器发送一个电信号。处理器一检测到此信号,便中断自己的当前工作转而处理中断。此后,处理器通知操作系统已经产生中断,这样,操作系统就可以对这个中断进行适当地处理了。

中断标志:不同设备对应的中断不同,每个中断都有一个唯一的数字标志。因此,来自键盘的中断有别于来自硬盘的中断,从而使得操作系统能够对中断进行区分,并指导是哪个硬件设备产生了哪个中断。这些中断值通常被称为中断请求线。

中断和异常的区别:

  • 异常在产生时必须考虑与处理器时钟同步。实际上,异常也常常称为同步中断。
  • 工作方式类似,中断是由硬件引起,异常是由软件引起。

2、中断处理程序

中断处理程序:当响应一个特定中断的时候,内核会执行一个函数,这个函数就是中断处理程序。

中断处理程序和其他内核函数的区别:中断处理程序是被内核调用来响应中断的,它们运行于中断上下文的特殊上下文中,又称为原子上下文,该上下文中的执行代码不可阻塞。

中断处理程序特点:

  • 中断可能随时发生,因此中断处理程序随时可能执行。所以必须保证中断处理程序能够快速执行,这样才能保证尽可能快地恢复中断代码的执行。
  • 中断处理程序的工作量可能也很大,像如今的千兆比特和万兆比特以太网卡。

3、上半部和下半部

中断分上半部和下半部的原因:又想中断处理程序运行得快,又想中断处理程序完成的工作量多,这两个目的有所矛盾,所以把中断处理切为两个部分。

  • 上半部:中断处理程序,接收到一个中断就立即开始执行,但只做有严格时限的工作,在所有中断被禁止的情况下完成。能够被允许稍后完成的工作会推迟到下半部去。
  • 下半部:合适的时机,下半部会被开中断执行。

4、注册中断程序

中断处理程序是管理硬件的驱动程序的组成部分。每一个设备都有相关的驱动程序,如果设备使用中断,那么相应的驱动程序就注册一个中断处理程序。

驱动程序可以通过request_irq()函数注册一个中断处理程序,并且激活给定的中断线,以处理中断。

/* request_irq:分配一条给定的中断线 */
int request_irq(unsigned int irq,			//要分配的中断号
				irq_handler_t handler,		//指针,指向实际中断处理程序
				unsigned long flags,		//标志位,可以设置模式
				const char *name,			//中断相关设备的ASCII文本表示
				void *dev)					//共享中断线
  • irq:要分配的中断号。
  • handler:是一个指针,指向处理这个中断的实际中断处理程序(回调)。只要操作系统一接收到中断,该函数就被调用。
  • flags:可以为0,也可以是一个或者多个标志的位掩码,定义在<linux/interrupt.h>中,较重要的有IRQF_DISABLED、IRQF_SAMPLE_RANDOM、IRQF_TIMER、IRQF_SHARED等。
  • name:是与中断相关的设备的ASCII文本表示。这些名字会被/proc/irq和/proc/interrupts文件使用。
  • dev:用于共享中断线。当一个中断处理程序需要释放时,dev将提供唯一的标志信息(cookie),以便从共享中断线的诸多中断处理程序中删除指定的那一个。如果没有这个参数,那么内核不可能知道在给定的中断线上到底要删除哪一个处理程序。如果无需共享中断线,那么将该参数赋值为空值(NULL)就可以了,但是,如果中断线是被共享的,那么就必须传递唯一的信息。另外,内核每次调用中断处理程序时,都会把这个指针传递给它。

共享中断线:因为多个设备共享同一个中断线号,当中断产生的时候到底是那一个设备产生的中断呢,这个就取决于第四个参数dev_id,这个参数必须是唯一的,也就是能区分到底是那个设备产生的中断,而且从第二个参数可以看出来,这个参数被传入中断处理程序(第二个参数),可以这么理解,当中断产生的时候,如果是共享的中断线号,则对应链表的所有中断处理程序都被调用,不过在每个中断处理程序的内部首先检查(参数信息以及设备硬件的支持)是不是这个中断处理程序对应的设备产生的中断,如果不是,立即返回,如果是,则处理完成,如果链表中没有一个是,则说明出现错误。

request_irq()函数可能会睡眠:在注册过程中,内核需要在/proc/irq文件中创建一个与中断对应的项。函数proc_mkdir()就是用来创建这个新的procfs项的。proc_mkdir()通过调用函数proc_create()对这个新的profs项进行设置,而proc_create()会调用函数kmalloc()来请求分配内存–函数kmalloc()是可以睡眠的。

释放中断处理程序:卸载驱动程序时,需要注销相应的中断处理程序,并释放中断线。可调用void free_irq(unsigned int irq, void *dev);如果指定的中断线不是共享的,那么该函数删除处理程序的同时将禁用这条中断线。如果中断线是共享的,则仅删除dev所对应的处理程序,则这条中断线本身只有在删除了最后一个处理程序才会被禁用。所以对于共享的中断线,唯一的dev是很重要的。

5、编写自己的中断程序

中断处理程序的声明:static irqreturn_t intr_handler(int irq, void *dev);

  • 第一个参数irq就使这个处理程序要响应的中断的中断号。
  • 第二个参数dev是一个通用指针,与中断处理程序注册时传递给request_irq()的参数dev必须一致。 如果该值由唯一确定性(为了能支持共享),那么它就相当于一个cookie,可以用来区分共享同一中断处理程序的多个设备。
  • 返回值irqreturn_t:当中断处理程序检测到一个中断,但该中断对应的设备并不是在注册处理函数期间指定的产生源时返I回IRQ_NONE;当中断处理程序被正确调用,且确实是它锁对应的设备产生了中断时,返回IRQ_HANDLED。

重入和中断处理程序:当一个给定的中断处理程序正在执行时,相应的中断线在所有处理器上都会被屏蔽掉,以防止在同一中断上接收另一个新的中断。通常情况下,所有其他的中断都是打开的,所以这些不同中断线上的其他中断都能被处理,但当前中断线总是被禁止的。

中断处理程序的共享与否在注册和运行方式上比较相似,但差异有如下几点,所有共享中断线的驱动程序也都必须满足以下要求:

  1. request_irq()的参数flags必须设置IRQF_SHARED标志;
  2. 对于每个注册的中断处理程序来说,dev参数必须唯一。指向任意设备结构的指针就可以满足这一要求:通常会选择设备结构,因为它是唯一的,而且中断处理程序可能会用到。不能给共享的处理程序传递NULL值;
  3. 中断处理程序必须能够区分它的设备是否真的产生了中断,也就要求中断处理程序必须知道是与它对于的设备发出了中断,还是共享这条中断线的其他设备发出了这个中断,这既需要硬件的支持,也需要处理程序中有相关的逻辑处理。

指定IRQF_SHARED标志以调用request_irq()时,只有在以下两种情况才可能成功:中断线当前未被注册,或者在该线上的所有已注册处理程序都指定了IRQF_SHARED。

6、中断上下文

进程上下文可以睡眠,也可以调用调度程序。中断上下文不可以睡眠,因为没有后备进程,所以中断处理程序不能调用睡眠的函数。时间限制中断上下文具有较为严格的时间限制,因为它打断了其它代码,所以中断上下文应当迅速、简洁。

中断栈:为了应对栈大小的减少,中断处理程序拥有了自己的栈(以前共享的内核栈),每个处理器一个,大小为一页。这个栈就称为中断栈。

7、中断处理机制的实现

在内核中,中断的旅程开始于预定义入口点,类似于系统调用通过预定义的异常句柄进入内核。对于每条中断线,处理器都会跳到对应的一个唯一的位置。这样,内核就可以知道所接收中断的IRQ号了。初始入口点只是在栈中保存这个号,并存放当前寄存器的值(这些值属于被中断的任务);然后,内核开始调用函数do_IRQ()。得到中断号后,do_IRQ()对所接收的中断进行应答,禁止这条线上的中断传递。然后,do_IRQ()需要确保在这条中断线上有一个有效的处理程序(中断处理程序),而且这个程序已经启动,但目前并没有执行。如果是这样,do_IRQ()就调用handle_IRQ_event()来运行为这条中断线所安装的中断处理程序。从handle_IRQ_event()回到do_IRQ(),该函数做清理工作并返回到初始入口点,然后再从这个入口点跳到函数ret_from_intr()该方法检查重新调度是否正在挂起。

中断从硬件到内核的路由:

8、中断控制

Linux内核提供了一组接口用于操作机器上的中断状态。这些接口为我们提供了能够进制当前处理器的中断系统,或屏蔽掉整个机器的一条中断线的能力。控制中断系统的原因归根结底是需要提供同步。通过禁止中断,可以确保某个中断处理程序不会抢占当前的代码。

9、中断下半部

中断处理流程分为两部分的原因:

  • 异步执行,会打断其他重要代码,避免被打断的代码停止时间过长,应该执行得越快越好
  • 上半部(中断处理程序)正在执行时,会屏蔽同级的中断甚至所有其他中断。禁止中断后硬件与操作系统无法通信。中断处理程序执行得越快越好。
  • 不在进程上下文运行,所以不能阻塞。限制了所做的事情。

在中断上下文运行的时候,当前的中断线在所有处理器上都会被屏蔽。更糟糕的是,如果一个处理程序是IRQF_DISABLED类型,它执行的时候会禁止所有本地中断(而且把本地中断线全局地屏蔽掉)。而缩短中断被屏蔽的时间对系统的响应能力和性能都至关重要。再加上中断处理程序要与其他程序(甚至是其他的中断处理程序)异步执行。必须尽量减少中断处理的执行,解决方法就是把一些工作放到”以后“去做。通常下半部在中断处理程序一返回就会马上运行。下半部执行的关键在于当它们运行的时候,允许响应所有的中断。

和上半部只能通过中断处理程序实现不同,下半部可以通过多种机制实现。这些用来实现下半部的机制分别由不同的接口和子系统组成。

处理器总处于以下三种状态之一:

  • 内核态,运行于进程上下文,内核代表进程运行于内核空间;
  • 内核态,运行于中断上下文,内核代表硬件运行于内核空间;
  • 用户态,运行于用户空间。

用户空间的应用程序,通过系统调用,进入内核空间。这个时候用户空间的进程要传递很多变量、参数的值给内核,内核态运行的时候也要保存用户进程的一些寄存 器值、变量等。所谓的“进程上下文”,可以看作是用户进程传递给内核的这些参数以及内核要保存的那一整套的变量和寄存器值和当时的环境等。

硬件通过触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的 一些变量和参数也要传递给内核,内核通过这些参数进行中断处理。所谓的“ 中断上下文”,其实也可以看作就是硬件传递过来的这些参数和内核需要保存的一些其他环境(主要是当前被打断执行的进程环境)。

10、下半部实现机制——软中断

软中断相对使用较少。tasklet是一种更加常用的形式。tasklet也是由软中断来实现的。使用void softirq_handler(struct softirq_action *)可以定义软中断的具体执行函数。软中断可以被中断处理程序中断。

一个注册的软中断必须在标记之后才会执行。下面的情况中待处理的软中断会被检查和执行

  • 从一个硬件中断代码返回时
  • 在ksoftirqd内核线程中
  • 在那些显式检查和执行待处理的软中断的代码中,如网络子系统。

在编译期间,使用的一种静态声明软中断。从0开始的索引表示一种优先级。索引号小的软中断优先执行。新的软中断必须在这个索引中添加自己的数据。下面是tasklet的类型:

使用如下函数进行软中断处理程序的注册:

  • open_softirq(NET_TX_SOFTIRQ,net_tx_action);
  • open_softirq(NET_RX_SOFTIRQ,net_tx_action);

因为软中断可以允许同时执行,因此其共享数据需要严格的锁来进行保护。因此适合在单处理器中的数据中进行。它允许在不同的多个处理器中进行任务,但是需要严格的锁控制。tasklet本质上也是软中断。不过不允许在多个处理器上同时运行。

使用raise_softirq()函数可以将一个软中断设置为挂起状态,等待do_softirq()函数的调用。

11、下半部实现机制——tasklet

它的接口更加简单,锁保护也更低。一般情况下推荐使用tasklet操作。

struct tasklet_struct
{
        struct tasklet_struct *next;    /* 链表中的下一个tasklet */
        unsigned long state;            /* tasklet的状态 */
        atomic_t count;                 /* 引用计数器 */
        void (*func)(unsigned long);    /* tasklet处理函数 */
        unsigned long data;             /* 给tasklet处理函数的参数 */
};

state成员只能在0、TASK_STATE_SCHED(已经被调度,准备运行)和TASK_STATE_RUN(正在运行)之间取值。counte为0时tasklet才能被激活,并被设置为挂起状态,该tasklet才能够执行。

已调度的tasklet被操作系统存放在两个链表队列中;tasklet_vec(普通tasklet)和tasklet_hi_vec(高优先级的tasklet)。然后由tasklet_schedule()和tasklet_hi_schedule()函数进行调度。前者使用TASKLET_SOFTIRQ后者使用HI_SOFTIRQ;下面是tasklet_schedule()函数的执行细节:

  • 检查tasklet状态是否为TASKLET_STATE_SCHED。是表示已经被调度,函数立刻返回。
  • 调用_tasklet_schedule()
  • 保存中断状态,然后禁止本地中断。保证数据的稳定
  • 将需要调度的tasklet添加到每个处理器的一个tasklet_vec链表或者task_hi_vec链表的表头上
  • 唤起TASKLET_SOFTIRQ或者HI_SOFTIRQ软中断,这样在下一次调用do_softirq()时就会执行该tasklet。
  • 恢复中断到原状态并返回。

一般最近一个中断返回时就是执行do_softirq()的最佳时机。TASKLET_SOFTIRQ和HI_SOFTIRQ已经被触发,do_softirq()会执行相应的软中断处理程序。关键在于tasklet_action()和tasklet_hi_action()。它们的工作内容如下:

  • 禁止中断,并为当前处理其检索tasklet_vec或tasklet_hi_vec链表。
  • 将当前处理器上的链表设置为NULL,达到清空的效果
  • 允许响应中断。没有必要再恢复它们回原状态,因为这段程序本身就是作为软中断处理程序被调用的,所以中断应该是被允许的。
  • 循环遍历获得链表上的每一个待处理的tasklet
  • 如果是多处理器气筒,通过检查TASKLET_STATE_SCHED来判断这个tasklet是否正在其它处理器上运行。如果正在运行就不要执行,跳到下一个。
  • 如果当前tasklet没有执行;将其状态设置为TASKLET_STATE_RUN,这样别的处理器就不会再去执行它。
  • 检查count值是否为0,确保tasklet没有被禁止。如果tasklet被禁止了,则跳到下一个挂起的tasklet去
  • 当tasket引用计数为0,并且没有在其它地方执行;则对其进行处理。
  • tasklet运行完毕,清除tasklet的state域的TASK_STATE_RUN 状态标志。
  • 重复执行下一个tasklet,直到没有剩余的等待处理的tasklet

使用void tasklet_handler(unsigned long data)来进行tasklet中断任务的设置。再使用tasklet_schedule(&my_tasklet)函数来传递tasklet并进行调度。为了防止tasklet被其它核上的处理器调度,可以使用tasklet_disable(&my_tasklet)禁止某个指定的tasklet被执行。然后使用tasklet_enable(&my_tasklet)来激活和进行下一步操作。

ksoftirqd:每个处理器都有一组辅助处理软中断(和tasklet)的内核线程。当任务量巨大时,通过内核进程对这个进行辅助处理。网络子系统中,软中断执行时可以重新触发自己以便再次得到执行。由于软中断可以重新触发自己,因此当大量软中断出现的时候;内核会唤醒一组内核线程(nice值是19,保证其在最低的优先级上运行)来处理任务。每个处理器中都有这样一个线程名为ksoftirqd/n(n)对应每个内核的编号。一旦线程被初始化,则会循环等待软中断的出现并进处理。

12、下半部实现机制——工作队列

工作队列可以将工作推后,让一个内核线程去执行;工作队列允许重新调度甚至睡眠。当工作队列中断后需要睡眠时,会优先选择它否则就优先选择软中断或者tasklet。后半部分的执行应该优先选择工作队列。

工作队列子系统是一个用于创建内核线程的接口。通过它创建的进程负责执行由内核其它部分排到队列里的任务。这个被称为工作者线程(work thread)。工作队列主要是让你的驱动程序创建一个专门的工作者线程类处理需要推后的工作。工作队列子系统提供了一个缺省的工作者线程来处理这些工作。

缺省的工作者线程叫做events/n,这里n表示处理器的编号。每个cpu核中有一个对应的工作队列线程,接受工作总队列的队列并,加入到自己的CPU队列中。单处理器系统中只有一个这样的线程。所有的工作者线程是用普通的内核线程实现的,它们都要执行work_thread()函数,在初始化完成之后就会在一个死循环中开始休眠直到,有操作被插入到队列中,线程才会被唤醒。执行之后继续休眠。每个处理器上的工作队列链表都是由上述工作结构组成的。当一个工作者线程被唤醒时,它就会执行它链表上的所有工作。工作被执行完毕就将这个work_struct对象从链表中移除。不再有对象的时候就会继续休眠。

创建推后的工作

  • DECLARE_WORK(name,void (*func) (void*),void data):静态创建一个名为name,处理函数为func,参数为data的结构体
  • INIT_WOK(struct work_struct *work,void (*func) (void *),void *data):动态的初始化一个由work指向的工作,处理函数为func,参数为data

工作队列处理函数

  • void work_handler(void *data):工作队列处理函数。函数会运行在进程上下文中。允许相应中断,并不持有任何锁。函数可以睡眠但是不能访问用户空间–内核线程在用户空间没有相关的内存映射。由系统调用进入用户态时(即用户态返回),它才能访问用户空间。

对工作进行调度

  • void schedule_work(&work):work马上会被调度。
  • void schedule_delayed_work(&work,delay):延迟delay之后再进行执行。

刷新操作

  • void flush_scheduled_work(void):刷新指定工作队列函数,函数会一直等待,直到队列中所有对象都被执行之后在返回。等待过程中函数会进入休眠状态。因此只能在进程上下文中使用(进程上下文与中断上下文的理解)
  • int cancel_delayed_work(struct work_struct *work):取消任何与work_struct相关的挂起工作

进程上下文

  • 进程上文: 其是指进程由用户态切换到内核态是需要保存用户态时cpu寄存器中的值,进程状态以及堆栈上的内容,即保存当前进程的进程上下文,以便再次执行该进程时,能够恢复切换时的状态,继续执行。
  • 进程下文: 其是指切换到内核态后执行的程序,即进程运行在内核空间的部分。

中断上下文

  • 中断上文: 硬件通过中断触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的一些变量和参数也要传递给内核,内核通过这些参数进行中断处理。中断上文可以看作就是硬件传递过来的这些参数和内核需要保存的一些其他环境(主要是当前被中断的进程环境)。
  • 中断下文: 执行在内核空间的中断服务程序。

13、下半部机制的选择

  • 软中断:多线索化工作良好。比如网络子系统。在多个处理器上并发的运行。适合专注于性能的提升。
  • tasklet:多线索化考虑得并不充分。如驱动程序。
  • 工作队列:将任务推后到进程上下文中完成。

14、下半部加锁和禁止下半部

下半部加锁:tasklet自己负责执行的序列化保障;两个相同类型的tasklet不允许同时执行,即使在不同的处理器上也不行。两个tasklet之间的同步(两个不同的tasklet共享同一数据时,需要正确使用锁机制)。如果进程上下文和一个下半部分共享数据,在访问这些数据之前,你需要禁止下半部分的处理并得到锁的使用权;防止本地和SMP的保护并防止死锁的出现。如果进程上下文和一个下半部分共享数据,在数据之前,你需要禁止中断并得到锁的使用权;防止本地和SMP的保护并防止死锁的出现。

禁止下半部:为了保护共享数据的安全,一般是先得到一个锁再禁止下半部分(所有软中断和所有的tasklet)的处理。函数通过为每个进程维护一个preempt_count为每个进程维护一个计数器。当计数器变为0时,下半部分才能够被处理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值