1、中断的基本概念
1.1 什么是中断
所谓中断是CPU在执行的程中,出现某些突发事件时CPU必须暂停执行当前的程序,转去处理突发
事件,处理完毕后CPU又返回原程序被中断的位置继续执行。
1.2 中断的分类
根据中断源的不同,可以把中断分为硬件中断和软件中断两大类,而硬件中断又可以分为外部中断和内部中断两类。
根据是否可以屏蔽分为可屏蔽中断和不可屏蔽中断,可屏蔽中断可以通过屏蔽字被屏蔽,屏蔽后,该中断不再得到响应,而不可屏蔽中断不能被屏蔽。
根据中断入口跳转方法的不同,中断又可以分为向量中断和非向量中断。
1.3 异常与中断
异常与中断不同,它在产生时必须考虑与处理器时钟同步。实际上,异常也称为同补中断。比如,在处理器执行到由于编程失误而导致的错误指令的时候,或者在执行期间出现特殊情况(缺页),必须靠内核来处理的,处理器就产生一个异常。中断的的工作方式类似,其差异只在于中断是由硬件而不是软件引起的。
1.4 中断处理程序
在响应一个特定中断的时候,内核会执行一个函数,该函数叫中断处理程序(interrupt handler)或中断服务例程(interrupt service routine,ISR)。产生中断的每个设备都有一个相应的中断处理程序。一个设备的中断处理程序是它设备驱动程序的一部分。中断处理程序与其他内核的真正区别在于:中断处理程序是被内核调用来响应中断的,而它们运行于我们称之为中断上下文的特殊上下文中。
中断处理程序与其他内核函数的的真正区别在于:中断处理程序是被内核调用来响应中断的,而他们运行于我们称之为中断上下文的特殊文件中。
1.5 上半部与下半部的对比
又想程序运行得快,又想程序完成的工作量太多,这两个目的相互矛盾。鉴于两个目的之间存在不可调和的矛盾,所以需要把中断处理程序分成两半或两个部分。中断处理程序是上半部(top half):接收到一个中断,他就立即开始执行,但只做严格时限的工作,例如对接收的中断进行应答或复位硬件,这些工作都是在所有中断被禁止的情况下完成的。能够被允许稍后完成的工作会推迟到下半部(bottom half)去。此后,在合适的时机,下半部被开中断执行。
2、中断控制函数
Linux内核提供了一组接口用于操作机器上的中断状态。可以在<asm/system.h>和<asm/irq.h>中找到。一般来说,控制中断系统的原因是需要提供同步。通过禁止中断,可以确保某个中断处理程序不会抢占当前的代码。此外,禁止中断还可以禁止内核抢占。
2.1 注册中断处理程序
驱动程序可以通过下面的函数注册并激活一个中断处理程序,以便处理中断:
int request_irq(unsigned int irq,
irqretrun_t (*handler)(int,void *, struct pt_regs *),
unsigned long irqflags,
const char *devname,
void *dev_id);
第一个参数irq表示要分配的中断号。对于大多数其他设备来说,这个值要么是可以通过探测获取,要么可以通过编程动态确定。
第二个参数hanlder是一个指针,指向处理这个中断的实际中断处理程序。hanhler函数的原型接收三个参数。并有一个类型为irqreturn_t的返回值。
第三个参数irqflags可以是0,也可以是多个标志的掩码。
SA_INTERRUPT:表明给定的中断处理程序是一个快速中断处理程序(fast interrupt hanlder)。使用了该标志,快速中断处理程序在禁止所有中断的情况下的本地处理器上运行。除了时钟中断,绝大数中断都不使用该标志。
SA_SAMPLE_RANDOM:表明这个设备产生的中断对内核熵池(entropy pool)有贡献。如果是SA_SHARE标志,表明可以在多个中断处理程序之间共享中断线。在同一个给定线上注册的每个处理程序必须指定这个标志。
第四个参数devname是与中断相关设备的ASCII文本表示法。这些名字会被/proc//irq和/proc/inerrupt文件使用,以便于用户通信。
第五个参数dev_id主要用户共享中断线。当一个中断处理程序需要释放时,dev_id将提供唯一的标志信息,以便从共享中断线的诸多中断处理程序中删除指定的那一个。如果无需共享中断线,那么将该参数赋为空值(NULL)就可以了。
该函数执行成功会返回0。如果返回非0值,就表示有错误发生。
注意:
request_irq函数可能会睡眠,因此,不能在中断上下文或其他不允许阻塞的代码中使用该函数。在注册的过程中,内核需要在/proc/irq文件中创建一个与中断对应的项。函数proc_mkdir就是用来创建这个新的procfs项的。函数proc_mkdir通过调用函数proc_mkdir通过调用proc_create对这个profs项进行设置,而proc_create会调用函数kmalloc函数请求分配内存。函数kmalloc是可以睡眠的。
2.2 释放中断处理程序
卸载驱动程序时,需要注销相应的中断处理程序,并释放中断线。free_irq()原型如下:
void free_irq(unsigned int irq, void * dev_id)
free_irq()中的参数与request_irq()相同
如果指定的中断线不是共享的,那么该函数删除处理程序的同时将禁用这条中断线。如果中断线是共享的,则仅删除dev_di对应的处理程序,而这条中断线只有在删除了最后一个处理程序时才会被禁用。
2.3 禁止和激活中断
用于禁止和激活当前处理器上的本地中断:
local_irq_disable();
local_irq_enable();
local_irq_save(unsigned long flags);
ocal_irq_restore(unsigned long flags);
前两个函数通常调用单个汇编指令来实现。实际上,在x86中它们分别使用cli指令和sti指令。如果在调用local_irq_disable例程之前已经禁止了中断,那么该例程往往带来潜在的危险;同样相应的local_irq_enable例程耶存在危险,因为他将无条件地激活中断,尽管这些中断可能在开始时就是关闭的。后两个函数可以保存现场,是系统更加安全。
内核2.5版本不再使用全局的cli,相应地,所有中断同步现在必须结合使用本地中断控制器和自旋锁。也就是说,为了确保对共享数据的互斥访问,现在需要做更多的工作。取消全局cli的优点:一是强制驱动程序编写实现真正的加锁,具有特定的细粒度比全局锁快许多;二是这使得很多代码更具流线型,避免了代码的成簇布局。
前面的所有函数既可以在中断中调用,也可以在进程上下文中调用。
2.4 禁止指定中断线
在某些情况下,只禁止整个系统中一条特定的中断线就够了。
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等待一个特定的中断处理程序的退出。如果该处理程序正在执行,那么该函数必须退出后才能返回。
对于这些函数的调用可以嵌套。其中有三个函数可以从中断或进程上下文中调用,而且不会睡眠。禁止多个中断处理程序共享的中断线是不合适的,禁止中断线也就禁止了这条线上所有设备的中断传递。因此,用于新设备的驱动程序应该倾向于不使用这些接口。
2.5 中断系统的状态
宏irqs_disable定义在<asm/system.h>中。如果本地处理器上的中断系统被禁止,则它返回非0,否则返回0。在<asm/hardirq.h>中定义的两个宏提供一个用来检测内核的当前上下文的接口:
int_interrupt()
int_irq()
第一个宏in_interrup最有用:如果内核处于中断上下文中,返回非0。说明内核此刻正在执行中断处理程序,或者正在执行下半部处理程序。宏in_irq只有在内核确实正在执行中断处理程序时返回非零。
3、编写中断处理程序
3.1 典型的中断处理程序声明:
static irqreturn_t intr_handler(int irq, void *dev_id, struct pt_regs *regs);
第一个参数irq就是这个处理程序要响应的中断的中断线号。
第二个参数dev_id是一个通用指针,它与在中断处理程序注册时传递request_irq的参数的dve_id必须一致。另外dev_id也可能指向中断处理程序使用的一个数据结构。因为,对于每个设备而言,设备结构是唯一的。
第三个参数regs是一个指向结构的指针,该结构包含处理中断之前处理器的寄存器和状态。考虑到现有的中断处理程序很少使用该参数,因此可以忽略它。
中断处理程序的返回值是一个特殊类型:irqreturn_t。中断处理程序可能会返回两个特殊的值:IRQ_NONE和IRQ_HANDLED。当中断处理程序检测到一个中断,但该中断对应的设备并不是在注册处理函数期间指定的产生源时,返回IRQ_NONE;当中断处理程序被正确调用,且确实是它所对应的设备产生了中断,返回IRQ_HANDLED。而实际上irqreturn_t就是一个int类型。
中断处理程序通常会标记为static,因为它从来不会被别人的文件中的代码直接调用。
3.2 重入和中断处理程序
Linux中的中断处理程序是无需重入的。当一个给定的中断处理程序正在执行时,相应的中断线在所有处理器上都会被屏蔽掉,以防止在同一中断线上接收另一个新的中断。
4、共享的中断处理程序
共享的处理程序与非共享的处理程序在注册和运行方式上比较类似,但差异如下:
1) request_irq的参数flags必须设置SA_SHARE标志
2) 对每个注册的中断处理程序来说,dev_id参数必须唯一
3) 中断处理程序必须能够区分它的设备是否真的产生了中断。这既需要硬件的支持,耶需要处理程序有相关的处理逻辑。
指定SA_SHARE标志以调用request_irq时,只有在以下两种情况下才可能成功:
1) 中断线当前未被注册
2) 在该线上的所有已经注册处理程序都指定了SA_SHARE。
内核接收一个中断后,它将依次调用在该中断线上注册的每一个处理程序。因此,一个处理程序应该必须知道它是否应该为这个负责。如果与它相关的设备并没有产生中断,那么处理器应该立即退出。
5、中断上下文
当执行一个中断处理程序或下半部时,内核处于中断上下文(interrupt context)中。中断上下文和进程没有关系,不可以睡眠。中断上下文具有严格的时间限制,因为它打断了其他代码。而进程上下文是一种内核所处的操作模式,此时内核代表进程执行,比如执行系统调用或运行内核线程。在进程上下文中,可以通过current宏关联当前进程,可以睡眠。
中断处理程序打断了其他代码,正是因为这种异步执行的特性,所以所有的中断处理程序必须尽可能的迅速、简洁。尽量把工作从中断处理程序中分离出来,交给下半部。
中断处理程序栈的设置是一个配置选项,决定中断处理程序是否共享中断进程的内核栈。内核栈的大小是两页。在2.6的内核中,增加一个选项,把栈的大小两页减到一页,这就减轻了内存的压力,因为系统中每个进程仅需要一页内核栈了。但是,为了应对栈大小的减少,中断处理程序拥有了自己的栈,每个处理器一个,大小为一页。这个栈称为中断栈。
6、中断处理机制的实现
设备产生中断,通过总线把电信号发送给中断控制器,处理器会立即停止它正在做的事,关闭中断系统,然后跳到内存中预定义的位置开始执行那里的代码。这个预定义的位置是由内核设置的,是中断处理程序的入口点。在内核栈,中断的旅程开始于预定义入口点,这类似于系统调用通过预定义的异常句柄进入内核。对于每条中断线,处理器都会跳到对应的一个唯一的位置。初始入口点只是在栈中保存这个号,并存放当前寄存器的值;然后,内核调用do_IRQ函数。
unsigned int do_IRQ(struct pt_regs regs);
该函数计算出中断号后,对所接收的中断进行应答,禁止这条线上的中断传递。在普通的PC机器上,这些操作由mask_and_ack_8259A来完成的。接着,该函数需要确保在这条中断线上有个有效的处理程序,而且这个程序已经启动,但是当前并没有执行。do_IRQ就调用handle_IRQ_event来运行为这条中断线安装的中断处理程序。最后,函数返回,回到do_IRQ。而do_IRQ做清理工作并返回到初始入口点,然后再从这个入口点跳到函数ret_from_intr函数。这个例程会检查重新调度是否正在挂起。如果重新调度正在挂起,而且内核正在返回用户空间(也就是中断了用户进程),那么schedule被调用。如果内核正在返回内核空间(也就是中断了内核本身),只有在preempt_count为0,schedule才会被调用。在schedule返回之后,或者没有挂起的工作,那么,原来的寄存器被恢复,内核恢复到曾经中断的点。
在x86上,初始的汇编例程位于arch/i386/kernel/entry.S,C方法在arch/i386/kernel/irq.c中。
6.1 文件/proc/interrupts
procfs是一个虚拟文件系统,它只存于内核内存,一般安装与/proc目录下。在procfs中读写都要调用内核函数,这些函数模拟从真实文件中读或写。
7、下半部机制
下半部的任务就是执行与中断处理密切相关但中断处理程序本身不执行的工作。比如,如果上半部把数据从硬件拷贝到内存,那么当然应该在下半部中处理它们。如何决定什么任务在哪部分完成取决于驱动程序开发者自己的判断。注意,中断处理程序会异步执行,并且在最好的情况下它会锁定当前的中断线。这里有些建议:
1) 如果一个任务队时间非常敏感,将其放在中断处理程序中执行
2) 如果一个任务和硬件相关,将其放在中断处理程序中执行
3) 如果一个任务要保证不被其他中断打断,将其放在中断处理程序中执行
4) 其他所有任务。考虑放在下半部执行
上半部简单快速,执行的时候禁止一些或者全部中断。下半部稍后执行,而且执行期间尅响应所有的中断。
Linux系统实现底半部机制主要有tasklet、工作队列和软中断
7.1 tasklet
tasklet是利用软中断实现的一种下半部机制。
7.1.1 tasklet结构体
struct tasklet_struct{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
该结构体位于文件<linux/interrupt.h>中。成员func域是tasklet的处理程序,data是它唯一的参数。
成员state只能在0,TASKLET_STATE_SCHED和TASKLET_STATE_RUN之间取值。TASKLET_STATE_SCHED表明tasklet已经被调度,正准备投入运行,TASKLET_STATE_RUN表明该tasklet正在运行。
成员count是tasklet的引用计数。如果它不为0,则tasklet被禁止,不允许执行;只有当它为0时,tasklet才被激活,并且在被设置为挂起状态时,该tasklet才能够执行。
7.1.2 调度tasklet
已经调度的tasklet存放在两个单处理器数据结构:tasklet_vec(普通tasklet)和tasklet_hi_vec(高优先级的tasklet)中。
tasklet由tasklet_schedule和tasklet_hi_schedule函数进行调度,它们接受一个指向tasklet_struct结构的指针作为参数。区别在于一个用于TASKLET_SOFTIRQ而一个用于HI_SOFTIRQ。先看tasklet_schedule函数的实现细节:
1) 检查tasklet的状态是否为TASKLET_STATE_SCHED。如果是,说明tasklet已经被调度过了,函数立即返回。
2) 保存中断状态,然后禁止本地中断。保证数据不会弄乱
3) 把需要调度的tasklet加到每个处理器一个的tasklet_vec或tasklet_hi_vec链表
4) 唤起TASKLET_SOFTIRQ和HI_SOFTIRQ软中断,这样在下一次调用do_softirq时就会执行该tasklet。
5) 恢复中断到原来状态并返回。
因为ASKLET_SOFTIRQ和HI_SOFTIRQ已经被触发了,所以do_softirq会执行相应的软中断处理程序。而这两个处理程序,tasklet_action和tasklet_hi_action,就是tasklet处理的核心。如下:
1) 禁止中断,并在当前处理器检索tasklet_vec或tasklet_hi_vec链表。
2) 将当前处理器上的该链表设置为NULL,达到清空的效果
3) 允许响应中断。
4) 循环遍历获得链表上的每一个待处理的tasklet。
5) 如果是多处理器系统,通过检查TASKLET_STATE_RUN状态标志来判断这个tasklet释放正在其他处理器上运行。如果它正在运行,那么现在就不要执行,跳到下一个待处理的tasklet去。
6) 如果当前这个tasklet没有执行,将其状态设置为TASKLET_STATE_RUN,这样别的处理器就不会再去执行它了。
7) 检查count是否为0,确保tasklet没有被禁止,如果tasklet被禁止,则跳到下一个挂起的tasklet去
8) 执行tasklet处理程序。
9) tasklet运行完毕,清除tasklet的state域的TASKLET_STATE_RUN状态标志
10) 重复执行下一个tasklet,直至没有剩余的等待处理的tasklet。
终上所述,所有的tasklet都通过重复运用TASKLET_SOFTIRQ和HI_SOFTIRQ这两个软中断来实现。当一个tasklet被调度时,内核会唤醒这两个软中断中断一个。随后,该软中断会被特定的函数处理,执行所有已经调度的tasklet。这个函数保证同一时刻只有一个给定类型的tasklet会被执行。
7.1.3 声明tasklet
如果你准备静态创建一个tasklet(直接引用),使用<linux/interrupt.h>中定义的两个宏的一个:
DECLARE_TASKLET(name,func,data);
DECLARE_TASKLET_DISABLED(name, func,data);
当该tasklet被调度以后,给定的函数func就会执行,它的参数由data给出。这两个宏的区别是引用计数器的初始值设置不同,前者设置为0,该tasklet处于激活状态,后者设置为1,tasklet处于禁止状态。例如
DECLARE_TASKLET(my_tasklet,my_tasklet_handler,dev);
动态创建一个tasklet(间接引用),使用下面接口:
tasklet_init(t, tasklet_handler,dev);
7.1.4 tasklet处理程序
tasklet处理程序必须符合规定的函数类型:
void tasklet_handler(unsigned long data);
因为是靠软中断来实现,所以tasklet不能睡眠,这就意味着不能在tasklet中使用信号量或其他非阻塞式函数。两个相同的tasklet决不会同时执行。
7.1.5 调度tasklet
通过调用tasklet_schedule函数传递它相应的tasklet_struct的指针,如下:
tasklet_schedule(&my_tasklet);
在tasklet被调度后,只要有机会它就会尽可能早地运行。如果在它还没有执行前又被调度了,它仍然只会运行一次。如果这时它已经运行了,比如说在另外一个处理器上,那么这个新的tasklet会被重新调度并再次运行。
可以调用tasklet_disable函数来禁止某个指定的tasklet。如果该tasklet当前正在执行,这个函数会等到它执行完毕再返回。也可以调用tasklet_disable_nosync函数,无需等待tasklet执行完毕。这往往不太安全,因为无法知道tasklet是否仍在执行。调用tasklet_enable可以激活一个tasklet。
可以调用tasklet_kill函数从挂起的队列中去掉一个tasklet。该函数的参数是一个指向某个tasklet的tasklet_struct的长指针。在处理一个经常重新调度它自身的tasklet的时候,这个函数很有用。这个函数首先等待该tasklet执行完毕,然后再将它移去。当然,无法阻止在其他地方的代码重新调度该tasklet。由于该函数可能会引起休眠,所以禁止在中断上下文中使用。
7.2 工作队列
工作队列(work queue)可以把工作推后,交由一个内核线程去执行,这个下半部总是会在进程上下文中执行。最重要的是工作队列允许重新调度甚至是睡眠。
如果你需要一个可以重新调度的实体来执行你的下半部处理,你应该使用工作队列。如果推后执行的任务需要睡眠,那么就选择工作队列。
7.2.1 工作队列的实现
工作队列子系统是一个用于创建内核线程的接口,通过它创建的进程负责执行由内核其他部分排到队列里的任务。它创建的这些内核线程被称为工作者线程(worker thread)。工作队列可以让你的驱动创建一个专门的工作者线程来处理需要推后的工作。也可以使用子系统提供的一个默认的工作者线程来处理。
默认的工作者线程叫做events/n,n是处理器的编号。每个处理器对应一个线程。
7.2.2. 表示线程的数据结构
工作者线程用workqueue_struct结构表示:
struct workqueue_struct{
struct cpu_workqueue_struct cpu_wq[NR_CPUS];
const char *name;
struct list_head list;
};
该结构是由一个cpu_workqueue_struct结构组成的数组,定义在kernel/workqueue.c中,数组的每一项对应系统中的一个处理器。cpu_workqueue_struct结构是kernel/workqueue.c中的核心数据结构:
struct cpu_workqueue_struct{
spinlock_t lock;
long remove_sequence; /*最近一个被加上的(下一个要运行的)*/
long insert_sequence; /*下一个要加上的*/
struct list_head worklist; /*工作列表*/
wait_queue_head_t more_work;
wait_queue_head_t work_done;
struct workqueue_struct *wq; /*有关联的workqueue_struct */
task_t *thread;
int run_depth; /*run_workqueue循环深度*/
};
注意:每个工作者线程类型关联一个自己的workqueue_struct。
7.2.3 表示工作的数据结构
所有的工作者线程都是用普通的内核线程实现的,它们都要执行worker_thread函数。在它初始化以后,这个函数执行一个死循环并开始休眠。当有操作被插入到队列里时,线程就会被唤醒,以便执行这些操作。当没有剩余的操作时,它又会休眠。
工作用<linux/workqueue.h>中定义的work_struct结构体表示。
struct work_struct{
unsigned long pending; /*这个工作正在等待处理吗?*/
struct list_head entry; /*连接所有工作的链表*/
void (*func)(void*); /*处理函数*/
void *data; /*传递处理函数的参数*/
void *wq_data; /*内部使用*/
struct timer_list timer; /*延迟的工作队列所用到的定时器*/
};
当工作者线程被唤醒,它会执行它的链表上的所有工作。工作被执行完毕,他就将相应的work_struct对象从链表上移去。
worker_thread函数的核心如下:
for(;;){
set_task_state(current, TASK_INTERRUPTIBLE);
add_wait_queue(&cwq->worklist);
if(list_empty(&cwq->worklist))
schedule();
else
set_task_state(current, TASK_RUNNING);
remove_wait_queue(&cwq->worklist &wait);
if(!list_empty(&cwq->worklist))
run_workqueue(cwq);
}
该函数在死循环中完成以下功能:
1) 线程将自己设置为休眠状态(TASK_INTERRUPTIBLE)并把自己加入到等待队列上
2) 如果工作链表是空的,线程调用schedule函数进入休眠
3) 如果链表中有对象,线程不会睡眠,它将自己设置为TASK_RUNNING,脱离等待队列。
4) 如果链表非空,调用run_workqueue函数执行被推后的工作。
7.2.4 run_workqueue函数
run_workqueue函数的核心实现如下:
while(!list_enpty(&cwq->worklist)){
struct work_struct *work;
void (*f)(void *);
void *data;
work=list_entry(cwq->worklist.next, struct work_struct, entry );
f = work->func;
data = work->data;
list_del_init(cwq->worklist.next);
crear_bit(0, &work->pending);
f(data);
};
该函数循环遍历链表上每个待处理的工作,执行链表每个节点上work_struct中的func函数:
1) 当链表不为空时,选取下一个节点对象
2) 获取希望执行的函数func及其参数
3) 把该节点从链表上解下来,将带处理标志位pending清0
4) 调用函数
5) 重复执行
7.2.5 工作队列的使用
7.2.5.1 创建推后的工作
首先要创建一些需要推后完成的工作。
编译时创建:
DECLARE_WORK(name, void (*func)(void*), void *data);
运行时创建:
INIT_WORK(struct work_struct *work, void (*func)(void*), void *data);
该函数动态初始化一个由work指向的工作。
7.2.5.2 工作队列处理函数
工作队列处理函数原型:
void work_handler(void *data);
这函数会由一个工作者线程执行,因此函数会运行在进程上下文中。默认下,允许响应汇总的,并且不持有任何锁。如果需要,可以睡眠。注意:尽管操作处理函数运行在进程上下文中,但它不能访问用户空间,因为内核线程在用户空间没有相关的内存映射。
7.2.5.3 对工作进行调度
工作已经创建后,就能调度它。想要把给定的工作处理函数提交给默认的events工作现场,只需要调用:
schedule_work(&work);
如果需要工作在指定的时间执行:
schedule_delayed_work(&work, delay);
7.2.5.4 刷新操作
排入队列的工作会在工作者线程下一次被唤醒的时候执行。内核准备了一个用于刷新指定工作队列的函数:
void flush_scheduled_work(void);
该函数会一直等待,直到队列中所有对象都被执行以后才返回。在等待所有待处理的工作执行时,该函数会进入睡眠,所以只能在进程上下文中使用,但不取消任何延迟执行的工作。
就是说,任何通过schedule_delayed_work调度的工作,如果其延迟时间未结束,它并不会因为调用flush_scheduled_work而被刷新掉。 取消延迟执行的工作应该调用:
int cancel_delayed_work(struct work_struct *work);
该函数可以取消任何与work_struct相关的挂起工作。
7.2.5.5 创建自己的工作队列
创建一个新的任务队列:
struct workqueue_struct *create_workqueu(const char *name);
name参数用于该内核线程的命名。例如,默认的events队列创建调用的是:
struct workqueue_struct *keventd_wq;
keventd_wq = create_workqueu(“events”);
该函数会创建所有的工作者线程(系统中每个处理器都有一个)并且做好所有开始处理工作之前的准备工作。
创建之后,可以调用如下函数创建工作:
int queue_work(struct workqueue_struct*wq, struct work_struct *work);
int queue_delayed_work(struct workqueue_struct*wq, struct work_struct *work,
unsigned long delay);
最后可以调用下面函数刷新指定的工作队列:
void flush_ workqueue(struct workqueue_struct*wq);
7.3 软中断
软中断使用得比较少;而tasklet是下半部更常用的一种形式。软中断的代码位于kernel/softirq.c中。
7.3.1 软中断的实现
软中断是在编译期间静态分配的。它由softirq_action结构体表示,定义在<linux/inerttupt.h>中。
struct softirq_action
{
void (*action) (struct softirq_action *);
void *data;
};
kernel/softirq.c中定义了一个32个该结构体的数组:
static struct softirq_action softirq_vec[32];
7.3.2 软中断处理程序
函数声明如下:
void softirq_handler(struct soffirq_action *);
当内核运行一个软中断处理程序时,它就会执行这个action函数,其唯一的参数就是为指向相应softirq_action结构体的指针。例如,如果my_action指向softirq_vec数组的某项,内核就会这样调用软中断:
my_action->action(my_action);
一个软中断不会抢占另外一个软中断。实际上,唯一可以抢占软中断的是中断处理程序。其他的软中断,甚至是相同类型的软中断,可以在其他处理器上同时执行。
7.3.3 执行软中断
一个注册的软中断必须在被标记后才会执行。这被称作触发软中断(raising the softirq)。通常,中断处理程序会在返回前标记它的软中断。在合适的时刻,待处理的会被检查和执行:
1) 从一个硬件中断代码返回时
2) 在ksoftirq内核线程中
3) 在那些显式检查和执行待处理的软中断代码中,如网络子系统
软中断都要在do_softirq中执行,其核心代码如下:
u32 pending = softirq_pending(cpu);
if (pending){
struct softirq_action *h= softirq_vec;
softirq_pending(cpu) = 0;
do {
if (pending &1)
h->action(h);
h++;
pending >>= 1;
}while(pending);
}
具体要做的包括:
1) 用局部变量pending保存softirq_pending宏返回的值。它是待处理的软中断的32位位图。如果第n位被置为1,那么第n位对应类型的软中断等待处理
2) 现在待处理的软中断位图已经保存,可以将实际的软中断位图清零了
3) 将指针h指向softirq_vec的第一项
4) 如果pending的第一位被置为1,h->action(h)被调用
5)指针加一
6) 位掩码右移一位
7) 现在指针h指向数组的第二项,重复上述操作
8) 一直重复下去,知道pending为0,表明已经没有待处理的软中断了
7.3.4 使用软中断
软中断保留给系统中对时间要求最严格的下半部使用,目前,只有两个子系统(网络和SCSI)直接使用软中断。而内核定时器和tasklet都是建立在软中断上的。
1) 分配索引
在编译期间,可以通过<linux/interrupt.h>中定义了一个枚举类型来静态声明软中断。
建立一个新的软中断必须在此枚举类型中加入新的项,注意顺序。习惯上,HI_SOFTIRQ是第一项,而TASKLET_SOFTIRQ是最后一项。
2) 注册你的处理程序
在运行时地调用open_softirq函数注册软中断处理程序,该函数有三个参数:软中断的索引号、处理函数和data域存放的数值。例如,网络子系统注册方式:
open_softirq(NET_TX_SOFTIRQ, net_tx_action, NULL);
open_softirq(NET_RX_SOFTIRQ, net_rx_action, NULL);
软中断处理程序执行的时候,允许响应中断,但它自己不能休眠。
3) 触发你的软中断
raise_softirq函数可以将一个软中断设置为挂起状态,让它在下次调用do_softirq函数时投入运行。例如,网络子系统调用如下接口:
raise_softirq(NET_TX_SOFTIRQ);
该函数在触发一个软中断之前先要禁止中断,触发后恢复原来的状态。如果中断本来就已经被禁止了,可以调用raise_softirq_irqoff函数来优化性能。
raise_softirq_irqoff (NET_TX_SOFTIRQ);
8、下半部机制的选择
在当前的2.6内核内核中,有三种可能的选择:软中断、tasklet和工作队列。tasklet是基于软中断实现,而工作队列机制靠内核线程实现。
软中断提供的执行序列化的保障最少,适用于对时间要求严格和执行频率很高的应用。驱动程序开发者应该尽可能选择tasklet而不是软中断,因为两个同种类型的tasklet不能同时执行。如果需要把任务推后到进程上下文中完成,就只能选择工作队列。
9、在下半部之间加锁
使用tasklet的一个好处在于它自己负责执行的序列化保障:两个相同类型的tasklet不允许同时执行,即使在不同的处理器也不行。tasklet之间的同步(就是两个不同类型的tasklet共享数据时)需要正确的锁。
因为软中断根本不保障执行序列化,所以所有的共享数据都需要合适的锁。
如果进程上下文和一个下半部共享数据,需要禁止下半部的处理并得到的使用权。
如果中断上下文和一个下半部共享数据,需要禁止中断并得到锁的使用权。
任何在工作队列中被共享的数据也需要使用锁机制。
9.1 禁止下半部
为了保证共享数据的安全,更常见的做法是先得到一个锁然后再禁止下半部的处理。如果需要禁止所有的下半部处理(就是本地所有的软中断和所有的tasklet),可以调用local_bh_disable函数。允许下半部进行处理,可以调用local_bh_enable函数。
这些函数有可能被嵌套使用。函数通过preempt_count为每个进程维护一个计数器。当计数器为0时,下半部才能够被处理。
但是,这些函数并不能禁止工作队列的执行。因为工作队列是在进程上下文中运行,不会涉及异步执行问题,所以没有必要禁止它们执行。由于软中断和tasklet是异步发生的,所以内核代码必须禁止它们。
10、总结
Linux的中断处理分为两个半部,上半部处理紧急的硬件操作,下半部处理不紧急的耗时操作。Tasklet和工作队列都是调度中断下半部的良好机制,tasklet基于软中断实现。在下半部中也会用到相当多的同步与并发。
11、参考文献:
Linux设备驱动开发详解
Linux内核设计与实现