各种硬件和处理器打交道的周期不同,并且总是比处理器慢。必须有一种可以让设备在产生某个事件时通知处理器----中断。

中断仅仅是一个信号,如果硬件需要,就可以发送这个信号。Linux处理中断方式和用户空间的信号是一样的。

注册一个中断,需要处理时,调用函数处理。

中断处理例程和其他代码并发运行,这样处理例程会不可避免的引起并发问题,并竞争数据结构和硬件。

一、安装和处理中断例程

中断信号线是非常珍贵且有限的资源,下列头文件<linux/sched.h>中声明的函数实现了该接口:

int request_irq(unsigned int irq,
irqreturn_t (*handler)(int, void *, struct pt_regs *),
unsigned long flags,
const char *dev_name,
void *dev_id);
void free_irq(unsigned int irq, void *dev_id);
/* requestid函数返回给请求函数的值为0,表示成功,负值为错误码
* unsigned int irq:要申请的中断号
* irqreturn_t (*handler)(int, void *, struct pt_regs *):这是要安装的中断处理函数指针
* const char *dev_name:传递给request_irq的字符串,用来在/proc/interrupts中显示中断的拥有者
* void *dev_id:这个指针用于共享的中断信号线
可以在flags中设置的位如下所示:
SA_INTERRUPT:当该位被设置时,表明这是一个“快递”的中断处理例程
SA_SHIRQ:该位表示中断可以在设备之间共享
SA_SAMPLE_RANDOM:该位指出产生中断能对/dev/random设备和/dev/urandom设别使用的熵池有贡献
*/

在模块初始化时安装中断例程,可能并没有使用到它,但是却占用了宝贵的资源。

所以在模块打开时注册中断,以保证共享这些有限的资源。

request_irq正确位置是设备第一次打开,硬件被告知中断之前。free_irq的位置式最后一次关闭设备,硬件被告知不用再中断之后。

这种技术的缺点是需要维护一个计数器,这样我们才能知道什么时候可以禁用中断。

/proc接口

产生的中断报告显示在文件/proc/interrupts中。

文件/proc/interrupts给出了已经发送到系统上每一个CPU的中断数量。

文件/proc/stat中给出了一些系统活动低层统计信息,包括(但不限于)从系统启动开始收到的中断数量。

自动检测IRQ号

驱动程序初始化时,最迫切的问题之一是如何决定设备将要使用哪条IRQ信号线。中断号的自动检测对于驱动程序可用性来说是一个基本要求。

理论上很简单,实际实现起来就不那么清晰了。我们看看执行该任务的两种方法:调用内核定义的辅助函数,或者实现我们自己的版本。

内核帮助下的探测

内核提供了一个底层设施来探测中断号,它只能在非共享中断的模式下工作,但大多数硬件有能力工作在共享中断模式下,并可提供更好的知道配置中断的方法。

#include <linux/interrupt.h>
unsigned long probe_irq_on(void);
返回一个未分配中断的位掩码
int probe_irq_off(unsigned long);
在请求设备产生中断之后,驱动程序调用这个函数。并将前面on返回的位掩码作参数传递给它
off返回on之后发生的中断编号

快速和慢速中断处理例程

老版本内核做了很多努力区分“快速和“慢速”中断。快速中断是那些可以很快被处理的中断,慢速中断则会花费更长的时间。

二、实现中断处理例程

中断处理例程和普通C程序没有区别,唯一特殊的地方就是处理器例程实在中断时间内运行的,它的行为会受到一些限制。

中断处理例程的功能就是将有关中断接收的信息反馈给设备,并根据正在服务的中断的不同含义对数据进行相应的读或写。

第一步通常需要清除接口卡上的一个位,大多数硬件设备在他们的"interrupt-pending"位被清除之前不回产生其他的中断。

中断例程典型的任务就是:如果中断通知进程所等待的事件已经发生,比如新的数据到达,就会唤醒在该设备上休眠的进程。

无论是快速还是慢速处理例程,程序员都应该编写执行事件尽可能短的处理例程。如果需要执行一个长时间的计算任务,最好的方法是使用tasklet或者工作队列在更安全的时间内调度计算任务。

short示例代码中,中断例程调用do_gettimeofday,并输出当前时间到大小为一页的循环缓冲区中,然后唤醒任何一个读进程,告诉该进程有新的数据可以换取。

irqreturn_t short_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
struct timeval tv;
int written; do_gettimeofday(&tv); /* 写入一个16字节的记录,假定PAGE_SIZE是16的倍数 */
written = sprintf((char *)short_head, "%08u.%06u\n",
(int)(tv.tv_sec % ), (int)(tv.tv_usec));
BUG_ON(written !=);
short_incr_bp(&short_head, written);
wake_up_interruptible(&short_queue); /*唤醒任何可读进程 */
return IRQ_HANDLED;
}

short_interrupt

调用的short_incr_bp函数定义如下:

static inline void short_incr_bp(volatile unsigned long *index, int data)
{
unsigned long new = *index + delta;
barrier(); /* 禁止对前后两条语句的优化 */
*index = (new >= (short_buffer + PAGE_SIZE)) ? short_buffer : new;
}

short_incr_bp

这个函数非常谨慎,它可以将指针限制在循环缓冲区的范围之内,并且不会因为传递一个不正确的值而返回。index可能会被编译器优化,barrier调用和volatile防止出现新的变量。

/dev/shortint的读取和写入

ssize_t short_i_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
int count0;
DEFINE_WAIT(wait); while(short_head == short_tail) {
prepare_to_wait(&short_queue, &wait, TASK_INTERRUPTIBLE);
if(short_head == short_tail)
schedule();
finish_wait(&short_queue, &wait);
if(signal_pending(current); /* 某个信号已到达 */
return -ERESTARTSYS; /* 告诉fs层来做进一步处理 */
}
/* count0 是可读取数据的字节数 */
count0 = short_head - short_tail;
if(count0 < ) /* 已交换 */
count0 = short_buffer + PAGE_SIZE - short_tail;
if(count0 < count) count = count0; if(copy_to_user(buf, (char *)short_tail, count))
return -EFAULT;
short_incr_bp(&short_tail, count);
return count;
} ssize_t short_i_write(struct file *filp, count char __user *buf, size_t count, loff_t *f_pos)
{
int written = , odd = *f_pos & ;
unsigned long port = short_base; /* 输出到并口的数据锁存器 */
void *address = (void *)short_base;
if(use_mem) {
while(written < count)
iowrite8(0xff * ((++written + odd) & ), address);
} else {
while(written < count)
outb(0xff * ((++written + odd) & ), port);
} *f_pos += count;
return written;
}

read and write

处理例程的参数及返回值

处理函数有三个参数被传递给了中断处理例程:irq、dev_id和regs。让我们看看每个参数的意义。

中断号(int irq)是可以打印到日志消息,第二个参数void *dev_id是一种客户数据类型(驱动程序的私有数据)。传递给request_irq函数的void *参数会在中断发生时作为参数被传回处理例程。通常dev_id传递一个纸箱自己设备的数据结构指针。这样多个设备的驱动程序在中断处理例程中不需要做任何额外的代码,就可以找出那个设备产生了当前中断事件

中断处理例程中参数的典型用法:

static irqreturn_t sample_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
struct sample_dev *dev = dev_id;
/* 现在,dev指向正确的硬件项 */
/* ... */
}

sample_interrupt

与这个处理例程相关联的典型open代码:

static void sample_open(struct inode *inode, stuct file *filp)
{
struct sample_dev *dev = hwinfo + MINO(indoe->i_rdev);
request_irq(dev->irq, sample_interrupt,
/* flags */, "sample", dev /* dev_id */); /* ... */
return ;
}

sample_open

最后一个参数struct pt_reg *regs很少使用,它保存了处理器进入中断代码之前的处理器上下文快照。

中断处理例程应该返回一个值,用来指明是否真正处理了一个中断。需要处理返回IRQ_HANDLED,否则,返回IRQ_NONE。

也可以通过,如果要处理handled非0

IRQ_RETVAL(handled)

启用和禁用中断

驱动程序中应尽量少禁用中断,同时这种技术不用在驱动程序汇总作为互斥机制使用。

禁用单个中断

#include <asm/irq.h>
void disable_irq(int irq);
void disable_irq_nosync(int irq);
void enable_irq(int irq);

禁用函数

调用这些函数会更新可编程中断控制器(programmable interrupt controller, PIC)中指定中断的掩码,因而就可以在所有的处理器上禁用或者启用IRQ。

这些函数的调用是可以嵌套的,调用disable_irq两次成功,那么在IRQ重启前,需要调用两次enable_irq

禁用所有的中断

#include <asm/system.h>
void local_irq_save(unsigned long flags);
void local_irq_disable(void); /* 打开 */
void local_irq_restore(unsigned long flags);
void local_irq_enable(void);

函数原型

三、顶半部和底半部

中断处理的一个主要问题是怎样在处理例程内完成耗时的任务。响应一次设备中断需要完成一定数量的工作,但是中断处理例程需要尽快结束而不能使中断阻塞的时间过长。

LInux将中断处理例程分成两部分解决这个问题。顶半部实际响应中断的例程,底半部是一个被顶半部调度,并在稍后更安全的时间内执行的例程。

这之间的区别在于,底半部处理例程执行时,所有的中断都是打开的。典型的情况就是顶半部保存设别的数据到一个设备特定的缓冲区并调度它的底半部,然后退出。

底半部有两种处理机制:tasklet和工作队列。

tasklet

tasklet是一个可以在由系统决定的安全时刻在软件中断上下文被调度运行的特殊函数。它可以被多次调度运行,但tasklet不会积累,只运行一次。

所以如果驱动程序有多个tasklet,他们必须使用某种锁机制来避免彼此间的冲突。

tasklet运行时,当然可以有其他的中断发生,因此在tasklet和中断处理例程之间的锁还是需要的。

必须使用宏DECLARE_TASKLET声明tasklet:

DECLARE_TASKLET(name, function, data);
name:是给tasklet起的名字
function:是执行tasklet时调用的函数(它有一个unsigned long型的参数并且返回void)
data:是一个用来传递给tasklet函数的unsigned long类型的值

DECLARE_TASKLET

驱动程序short如下声明它自己的tasklet:

void short_do_tasklet(usnigned long);
DECLARE_TASKLET(short_tasklet, short_do_tasklet, );

DECLARE_TASKLET

这个处理例程保存数据并如下调度tasklet:

irqreturn_t short_tl_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
do_gettimeofday((struct timeval *)tv_head); /* 强制转换以免出现“易失性”警告 */
short_incr_tv(&tv_head);
tasklet_schedule(&short_tasklet);
short_wq_count++; /* 记录中断的产生 */
return IRQ_HANDLED;
}

short_tl_interrupt

这个例程执行中断处理的大多数任务,如下所示:

void short_do_tasklet(unsigned long unused)
{
int savecount = short_wq_count, written;
short_wq_count = ; /* 已经从队列中移除 */
/*
* 底半部读取由顶半部填充的tv数组,
* 并向循环文本缓冲区中打印信息,而缓冲区的数据则由
* 读取进程获得
*/ /* 首先将调用此bh之前发生的中断数量写入 */
written = sprintf((char *)short_head, "bh after %6i\n", savecount);
short_incr_bp(&short_head, written); /*
* 然后写入时间值,每次写入16字节
* 所以它与PAGE_SIZE是对齐的
*/
do {
written = sprinf((char *)short_head, "%08u.%06u\n",
(int)(tv_tail->tv_sec % ),
(int)(tv_tail->tv_usec));
short_incr_bp(&hsort_head, written);
short_incr_tv(&tv_tail);
} while(tv_tail != tv_head);
wake_up_interruptible(&short_queue); /* 唤醒任何读取进程 */
}

short_do_tasklet

工作队列

工作队列会在将来的某个事件、在某个特殊的工作者进程上下文中调用一个函数。

工作队列函数运行在进程上下文中,必要时可以休眠。但是我们不能从工作队列向用户空间复制数据,除非使用高级技术。

work_struct结构,该结构如下声明并初始化:

static struct work_struct short_wq;
/* 下面这行出现在short_init()中 */
INIT_WORK(&short_wq, (void (*)(void *))short_do_tasklet, NULL);

在使用工作队列时,short构造了另一个中断处理例程,如下:

irqreturn_t short_wq_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
/* 获取当前的时间信息 */
do_gettimeofday((struct timeval *)tv_head);
short_incr_tv(&tv_head); /* 排序bh,不必关心多次调度的情况 */
schedule_work(&short_wq); short_wq_count++; /* 记录中断的到达 */
return IRQ_HANDLED;
}

short_wq_interrupt

四、中断共享

现在现代硬件已经能允许中断的共享了。

安装共享的处理例程

  • 请求中断时,必须指定falgs参数中的SA_SHIRQ位
  • dev_id参数必须是唯一的。任何指向模块地址空间的指针都可以使用,但dev_id不能设置成NULL

当请求一个共享中断时,如果满足下面条件,request_irq就会成功。

  • 中断信号线空闲
  • 任何已经注册了该中断信号线的处理例程也标识了IRQ是共享的

当两个或者更多的驱动程序共享同一个根中断信号线,而硬件又通过这根信号线中断处理器时,内核会调用每一个为这个中断注册的处理例程,并将它们自己的dev_id传回去。

运行处理例程

irqreturn_t short_sh_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
int value, written;
struct timeval tv; /* 如果不是short产生的,则立即返回 */
value = inb(short_base);
if(!(value & 0x80))
return IRQ_NONE; /* 清除中断位 */
outb(value & 0x7F, short_base); /* 其余部分没有什么变化 */
do_gettimeofday(&tv);
written = sprintf((char *)short_head, "08u.%06u\n",
(int)(tv.tv_sec % ), (int)(tv.tv_usec));
short_incr_bp(&short_head, written);
wake_up_interruptible(&short_queue); /* 唤醒任何的读取进程 */
return IRQ_HANDLED;
}

short_sh_interrupt

/proc接口和共享的中断

系统上安装的中断处理例程不会对/proc/stat造成影响,所有为同一个中断号安装的处理例程会出现在/proc/interrupts文件的同一行上。

中断驱动的I/O

如果与驱动程序管理的硬件之间的数据传输因为某种原因延迟的话,驱动程序作者就应该实现缓冲。数据缓冲区有助于将数据传送和接收与系统调用write和read分离开来,从而提高系统的整体性能。

一个好的缓冲机制需要采用中断驱动的I/O,一个输入缓冲区在中断时间内被填充,并由读取该设备的进程取走缓冲区内的数据。一个输出缓冲区由写入设备的进程填充,并在中断时间内取走数据。

要正确进行中断驱动的数据传输,要求硬件能按照下面的语义来产生中断:

  • 对于输入来说,新数据到达并且准备好接收时,设备就中断CPU。实际执行的动作取决于设备使用的是I/O端口、内存映射、还是DMA。
  • 对于输出来说,当设备准备好接收新数据或对陈功的数据传输进行应答时,就是发出中断。内存映射和具有DMA能力的设备,通常通过产生中断来通知系统他们对缓冲区的处理已经结束。

写缓冲区示例

while(written < count) {
/* 挂起直到有可用缓冲区空间为止 */
space = shortp_out_space();
if(space <= ) {
if(wait_event_interruptible(shortp_out_queue,
(sapce = shortp_out_space()) > )
goto out;
}
/* 将数据移动到缓冲区 */
if((space + written) > count)
space = count - written;
if(copy_from_user((char *)shortp_out_head, buf, space)) {
up(&shortp_out_sem);
return -EFAULT;
}
shorp_incr_out_bp(&shortp_out_head, space);
buf += space;
written += space;
/* 如果没有激活的输出,则激活 */
spin_lock_irqsave(&shortp_out_lock, flags);
if(!shortp_output_active)
shortp_start_output();
spin_unlock_irqrestore(&shortp_out_lock, flags);
}
out:
*f_pos += written;

写缓冲

启动输出进程的函数如下所示:

static void shortp_start_output(void)
{
if(shortp_output_active) /* 不应发生 */
return; /* 设置“丢失中断”定时器 */
shorp_output_active = ;
shorp_timer.expires = jiffies + TIMEOUT;
add_timer(&shortp_timer); /* 然后让进程继续 */
queue_work(shortp_workqueue, &shortp_work);
}

shortp_start_output

有时候会丢失来自设备的中断,我们不希望永久停止直到系统重启。利用内核定时器意识到中断丢失并继续处理则会好得多。其核心功能如下:

spin_lock_irqsave(&shortp_out_lock, flags);

/* 是否有数据写入 */
if(shortp_out_head == shortp_out_tail) { /* 空的 */
shortp_output_active = ;
wake_up_interruptible(&shortp_empty_queue);
del_timer(&shortp_timer);
}
/* 否则写入其他字节 */
else
shortp_do_write(); /* 如果有人等待,则唤醒之。 */
if(((PAGE_SIZE + shortp_out_tail - shortp_out_head) % PAGE_SIZE) > SP_MIN_SPACE) {
wake_up_interruptible(&shortp_out_queue);
}
spin_unlock_irqrestore(&shortp_out_lock, flags);

核心代码

数据真正写入端口的代码:

static void shortp_do_write(void)
{
unsigned char cr = inb(shortp_base + SP_CONTROL); /* 有情况发生,重置定时器 */
mod_timer(&shortp_timer, jiffies + TIMEOUT);
/* 想设备输出一个字节 */
outb_p(*shortp_out_tail, shortp_base + SP_DATA);
shortp_incr_out_bp(&shortp_out_tail, );
if(shortp_delay)
udelay(shortp_delay);
outb_p(cr | SP_CR_STROBE, shortp_base + SP_CONTROL);
if(shortp_delay)
udelay(shortp_delay);
outb_p(cr & ~SP_CR_STROBE, shortp_base + SP_CONTROL);
}

shortp_do_write

然后会中CPU,shortprint使用的汇总单处理例程很短,也很简单:

static irqreturn_t shortp_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
if(!shortp_output_active)
return IRQ_NONE;
/* 记录时间,其他信息在工作队列函数中获得 */
do_gettimeofday(&shortp_tv);
queue_work(shortp_workqueue, &shortp_work);
return IRQ_HANDLED;
}

shortp_interrupt

如果中断始终不产生会怎样呢?驱动程序代码会导致停顿。为了避免这种情况发生

static void shortp_timeout(unsigned long unused)
{
unsigned long flags;
unsigned char status; if(!shortp_output_active)
return;
spin_lock_irqsave(&shortp_out_lock, flags);
status = inb(shortp_base + SP_STATUS); /* 如果打印机任然忙,则只是重置定时器 */
if((status & SP_SR_BUSY) == || (status & SP_SR_ACK)) {
shortp_timer.expires = jiffies + TIMEOUT;
add_timer(&shortp_timer);
spin_unlock_irqqrestore(&shortp_out_lock, flags);
return;
}
/* 否则必须调解中断处理例程 */
spin_unlock_irqrestore(&shortp_out_lock, flags);
shortp_interrupt(shortp_irq, NULL, NULL);
}

shortp_timeout

LDD3 第10章 中断处理的更多相关文章

  1. 《构建之法》之第8、9、10章读后感 ,以及sprint总结

    第8章: 主要介绍了软件需求的类型.利益相关者,获取用户需求分析的常用方法与步骤.竞争性需求分析的框架NABCD,四象限方法以及项目计划和估计的技术. 1.软件需求:人们为了解决现实社会和生活中的各种 ...

  2. 敏捷软件开发:原则、模式与实践——第10章 LSP:Liskov替换原则

    第10章 LSP:Liskov替换原则    Liskov替换原则:子类型(subtype)必须能够替换掉它们的基类型(base type). 10.1 违反LSP的情形 10.1.1 简单例子 对L ...

  3. 孙鑫视频学习:对第10章设置线宽时为什么不调用UpDateData(TRUE)的理解

    在第10章10.2.1小节中,首先分别对视图类和对话框类添加了一个名为m_nLineWidth的int型变量,再将用户在CSetting dlg对话框的edit控件中输入的线宽值记录在dlg.m_nL ...

  4. 第10章 系统级I/O

    第10章 系统级I/O 10.1 Unix I/O 一个Unix文件就是一个m个字节的序列:B0,B1,…,BK,…,Bm-1 Unix I/O:一种将设备优雅地映射为文件的方式,允许Unix内核引出 ...

  5. 高性能Linux服务器 第10章 基于Linux服务器的性能分析与优化

    高性能Linux服务器 第10章    基于Linux服务器的性能分析与优化 作为一名Linux系统管理员,最主要的工作是优化系统配置,使应用在系统上以最优的状态运行.但硬件问题.软件问题.网络环境等 ...

  6. Linux就这个范儿 第10章 生死与共的兄弟

    Linux就这个范儿 第10章 生死与共的兄弟 就说Linux系统的开机.必须经过加载BIOS.读取MBR.Boot Loader.加载内核.启动init进程并确定运行等级.执行初始化脚本.启动内核模 ...

  7. 【翻译】《深入解析windows操作系统第6版下册》第10章:内存管理

    [翻译]<深入解析windows操作系统第6版下册>第10章:内存管理(第一部分) [翻译]<深入解析windows操作系统第6版下册>第10章:内存管理(第二部分) [翻译] ...

  8. 《构建之法》第8、9、10章读后感和Sprint总结

    <构建之法>第8.9.10章读后感  第八章重点讲了需求分析,在一个项目中,需求分析是最基础也是最重要的,只有充分了解了用户需求,我们才不会走弯路,才能做出正确的规划,保证项目的进行是按照 ...

  9. JavaScript高级程序设计(第三版)学习笔记8、9、10章

    第8章,BOM BOM的核心对象是window,具有双重角色,既是js访问浏览器的一个接口,又是ECMAScript规定的Global对象.因此,在全局作用域中声明的函数.变量都会变成window对象 ...

随机推荐

  1. spring boot创建多模块聚合工程

    环境:java1.8,idea 聚合工程优势: 1.统一maven操作.可以在一个maven工程管理多个子工程(每个子工程可单独打包,重启,调试.也可通过聚合工程一起管理). 2.统一管理依赖版本.可 ...

  2. 数据挖掘与CRM

    数据挖掘与CRM 现在的数据挖掘项目多数都是游击战,这边挖一挖那边挖一挖,挖到最后还是一场空,还落了个"忽悠"绰号:回想数据挖掘的一个标准流程,那只是一个数据挖掘类项目的标杆而已, ...

  3. sql 优化建议

    1.对查询进行优化,要尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引. 2.应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索 ...

  4. Oracle系列:触发器、作业、序列、连接

    .Net程序员学用Oracle系列(8):触发器.作业.序列.连接   1.触发器 2.作业 2.1.作业调度功能和应用 2.2.通过 DBMS_JOB 来调度作业 3.序列 3.1.创建序列 3.2 ...

  5. Vue动态添加响应式属性

    不能给Vue实例.Vue实例的根数据对象添加属性. 文件 <template> <div id="app"> <h2>{{hello}}:{{a ...

  6. Django csrf,xss,sql注入

    一.csrf跨站请求伪造(Cross-site request forgery) CSRF的攻击原理:简单说就是利用了高权限帐号(如管理员)的登录状态或者授权状态去做一些后台操作,但实际这些状态并没有 ...

  7. [Linux] 010 权限管理命令 chmod

    1. 权限管理命令:chmod 命令名称:chmod 命令英文原意:change the permissions mode of a file 命令所在路径:/bin/chmod 执行权限:所有用户 ...

  8. IDEA远程代码实时同步(可以自动实时同步)

    前言 开发时一般的平台都是windows,但windows对开发极其不友好,一般都会在本地开启虚拟机,安装上linux环境进行项目的部署测试.下面介绍一种windows主机与linux虚拟机代码同步的 ...

  9. each of which 用法

    each of which 在以下為 同位語,非關代. 1. An urn contains two balls, each of which is known to be either white ...

  10. 微信、qq网页二次分享

    二次分享是指,在APP或者浏览器分享到微信或者qq,然后从微信或者qq再分享到别的平台.如果不处理,再次分享出去的图片或者标题就不会显示,对用户非常不友好. 一.微信二次分享 官方接入文档:https ...