Linux设备驱动程序学习笔记
第十章 中断处理
一、安装中断处理例程
中断信号线是珍贵且有限的资源。内核维护了一个中断信号线的注册表,模块在使用中断前要现请求一个中断通道(或者中断请求IRQ),然后在使用后释放该通道。
#include <linux/sched.h>
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev)
//irq要申请的中断号
//irq_handler_t handler,typedef irqreturn_t (*irq_handler_t)(int, void *);这里是要安装的中断处理函数指针,后面讨论
//unsigned long flags中断管理有关的位掩码选项
//const char *name传递给request_irq的字符串,用来在/proc/interrupts中显示中断的拥有者(参见后面)
//void *dev用于共享的中断信号线。唯一的标识符,在中断信号线空闲时可以使用它,驱动程序也可以使用它指向驱动程序自己的私有数据区(用来识别哪个设备产生中断)
void free_irq(unsigned int irq, void *dev);
request_irq的正确位置是在设备的第一次打开、硬件被告知产生中断之前。
free_irq是最后一次关闭设备,硬件被告知不再用中断处理器之后。
if (short_irq >= 0) {
result = request_irq(short_irq, short_interrupt,
0, "short", NULL);
if (result) {
printk(KERN_INFO "short: can't get assigned irq %i\n",
short_irq);
short_irq = -1;
}
else { /* actually enable it -- assume this *is* a parallel port */
outb(0x10,short_base+2);
}
}
/proc接口
当硬件的中断到达处理器时,一个内部计数器递增,这位检查设备提供了方法
- cat /proc/interrupts
显示各个CPU在IRQ号上的发生的中断数量。但其只会显示那些已经安装了中断处理例程的中断。若有些中断处理例程时打开文件时安装则不会显示 - cat /proc/stat
一般比上一个方式更有用,可以显示所有中断号发生次数。并且依赖于体系结构。
自动监测IRQ号
驱动初始化时最迫切的问题之一就是如何确定设备将要使用哪一条IRQ信号线。因为用户大部分时间不知道指定哪个中断号。
大多数现在硬件设备有能力告诉驱动程序它将使用的中断线,比如PCI标准要求外设声明它们打算是用的中断线,但有些设备需要自动监测。
两种方法:
- 内核帮助下的探测
void short_kernelprobe(void)
{
int count = 0;
do {
unsigned long mask;
mask = probe_irq_on();//返回一个未分配中断的位掩码,用于传递该后面的probe_irq_off函数。调用该函数之后,驱动程序要安排设备产生至少一次中断
outb_p(0x10,short_base+2); /* 启用中断报告 */
outb_p(0x00,short_base); /* 清除该位 */
outb_p(0xFF,short_base); /* 设置该位:中断! */
outb_p(0x00,short_base+2); /* 禁止中断报告 */
udelay(5); /* 留给中断探测一些时间 */
short_irq = probe_irq_off(mask);
if (short_irq == 0) { /* none of them? */
printk(KERN_INFO "short: no irq reported by probe\n");
short_irq = -1;
}
/*
* 如果失败,重新尝试5次后放弃
*/
} while (short_irq < 0 && count++ < 5);
if (short_irq < 0)
printk("short: probe failed %i times, giving up\n", count);
}
- DIY探测
和上述原理一样,启用所有未被占用的中断,然后观察会发生什么。不同的是,应充分发挥对有关设备的了解,如例,我们假定可能的IRQ值是3,5,7,9
volatile int short_irq = -1;
irqreturn_t short_probing(int irq, void *dev_id)
{
if (short_irq == 0) short_irq = irq; /* found */
if (short_irq != irq) short_irq = -irq; /* ambiguous */
return IRQ_HANDLED;
}
void short_selfprobe(void)
{
int trials[] = {3, 5, 7, 9, 0};
int tried[] = {0, 0, 0, 0, 0};
int i, count = 0;
/*
* install the probing handler for all possible lines. Remember
* the result (0 for success, or -EBUSY) in order to only free
* what has been acquired
*/
for (i = 0; trials[i]; i++)
tried[i] = request_irq(trials[i], short_probing,
0, "short probe", NULL);
do {
short_irq = 0; /* none got, yet */
outb_p(0x10,short_base+2); /* enable */
outb_p(0x00,short_base);
outb_p(0xFF,short_base); /* toggle the bit */
outb_p(0x00,short_base+2); /* disable */
udelay(5); /* give it some time */
/* the value has been set by the handler */
if (short_irq == 0) { /* none of them? */
printk(KERN_INFO "short: no irq reported by probe\n");
}
/*
* If more than one line has been activated, the result is
* negative. We should service the interrupt (but the lpt port
* doesn't need it) and loop over again. Do it at most 5 times
*/
} while (short_irq <=0 && count++ < 5);
/* end of loop, uninstall the handler */
for (i = 0; trials[i]; i++)
if (tried[i] == 0)
free_irq(trials[i], NULL);
if (short_irq < 0)
printk("short: probe failed %i times, giving up\n", count);
}
快速和慢速处理例程
快中断SA_INTERRUPT执行时,当前处理器上其他所有中断被禁止,但其他处理器仍可处理中断。但在现代系统中,SA_INTERRUPT只在少数几种特殊情况如定时器中断使用,驱动开发应尽量不使用。
二、实现中断处理例程
特殊在于例程是在中断时间内运行的,行为受到一些限制:
处理例程不能向用户空间发送和接受数据,因为他不是在任何进程的上下文中执行。
处理例程不能做任何可能发生休眠的操作,如调用wait_event使用不带GFP_ATOMIC标志的内存分配操作,或者锁住一个信号量等等。
处理例程不能调用schdule函数。
三、启用和禁用中断
略
四、顶半部和底半部
tasklet通常是底半部处理的优选机制,这种机制非常快,但是所有tasklet代码必须是原子的。
工作队列具有更高的延迟,但允许休眠。
tasklet
tasklet是软中断的一种特殊函数,可以被多次调度运行,但tasklet的调度不会累积。
不会有同一个tasklet的多个实例并行地运行,但是可以与其他tasklet并行运行在SMP系统上。
声明:
DECLARE_TASKLET(name, func, data);
name是给tasklet的名字,func是执行tasklet时调用的函数(void (*func)(unsigned long);),data是一个用来传递给tasklet函数的unsigned long类型的值
tasklet_schedule函数用来调度一个tasklet运行。
void tasklet_schedule(struct tasklet_struct *t)
void short_do_tasklet(struct tasklet_struct *);
DECLARE_TASKLET(short_tasklet, short_do_tasklet);//Linux5.9版本更新
irqreturn_t short_tl_interrupt(int irq, void *dev_id)
{
ktime_get_real_ts64((struct timespec64 *) tv_head); /* cast to stop 'volatile' warning */
short_incr_tv(&tv_head);
tasklet_schedule(&short_tasklet);
short_wq_count++; /* record that an interrupt arrived */
return IRQ_HANDLED;
}
void short_do_tasklet (struct tasklet_struct * unused)
#endif
{
int savecount = short_wq_count, written;
short_wq_count = 0; /* we have already been removed from the queue */
/*
* The bottom half reads the tv array, filled by the top half,
* and prints it to the circular text buffer, which is then consumed
* by reading processes
*/
/* First write the number of interrupts that occurred before this bh */
written = sprintf((char *)short_head,"bh after %6i\n",savecount);
short_incr_bp(&short_head, written);
/*
* Then, write the time values. Write exactly 16 bytes at a time,
* so it aligns with PAGE_SIZE
*/
do {
written = sprintf((char *)short_head,"%08u.%06lu\n",
(int)(tv_tail->tv_sec % 100000000),
(int)(tv_tail->tv_nsec) / NSEC_PER_USEC);
short_incr_bp(&short_head, written);
short_incr_tv(&tv_tail);
} while (tv_tail != tv_head);
wake_up_interruptible(&short_queue); /* awake any reading process */
}
该tasklet记录了自它上次被调用以来产生的中断次数。
工作队列
static struct work_struct short_wq;
irqreturn_t short_wq_interrupt(int irq, void *dev_id)
{
/* Grab the current time information. */
ktime_get_real_ts64((struct timespec64 *) tv_head);
short_incr_tv(&tv_head);
/* Queue the bh. Don't worry about multiple enqueueing */
schedule_work(&short_wq);
short_wq_count++; /* record that an interrupt arrived */
return IRQ_HANDLED;
}
void short_do_tasklet (struct tasklet_struct * unused)
#endif
{
int savecount = short_wq_count, written;
short_wq_count = 0; /* we have already been removed from the queue */
/*
* The bottom half reads the tv array, filled by the top half,
* and prints it to the circular text buffer, which is then consumed
* by reading processes
*/
/* First write the number of interrupts that occurred before this bh */
written = sprintf((char *)short_head,"bh after %6i\n",savecount);
short_incr_bp(&short_head, written);
/*
* Then, write the time values. Write exactly 16 bytes at a time,
* so it aligns with PAGE_SIZE
*/
do {
written = sprintf((char *)short_head,"%08u.%06lu\n",
(int)(tv_tail->tv_sec % 100000000),
(int)(tv_tail->tv_nsec) / NSEC_PER_USEC);
short_incr_bp(&short_head, written);
short_incr_tv(&tv_tail);
} while (tv_tail != tv_head);
wake_up_interruptible(&short_queue); /* awake any reading process */
}
int short_init(void){
/*......*/
INIT_WORK(&short_wq, (void (*)(struct work_struct *)) short_do_tasklet);
if (short_irq >= 0 && (wq + tasklet) > 0) {
free_irq(short_irq,NULL);
result = request_irq(short_irq,
tasklet ? short_tl_interrupt :
short_wq_interrupt,
0, "short-bh", NULL);
if (result) {
printk(KERN_INFO "short-bh: can't get assigned irq %i\n",
short_irq);
short_irq = -1;
}
}
}
五、中断共享
安装共享的处理例程
与普通非共享中断的不同:
- 请求中断时,必须指定flags参数中的SA_SHIRQ位
- dev_id参数必须是唯一的。任何指向模块地址空间的指针都可以使用,但dev_id不能设置为NULL
两种情况request_irq会成功: - 中断信号线空闲
- 任何已经注册了该中断信号线的处理例程也标示了IRQ是共享的
free_irq就是通过dev_id
/proc接口和共享的中断
/proc/stat不会有影响,proc/interrupts会改变
即共享中断例程会共同显示在同一行
六、中断驱动的I/O
/*
* A version of the "short" driver which drives a parallel printer directly,
* with a lot of simplifying assumptions.
*
* Copyright (C) 2001 Alessandro Rubini and Jonathan Corbet
* Copyright (C) 2001 O'Reilly & Associates
*
* The source code in this file can be freely used, adapted,
* and redistributed in source or binary form, so long as an
* acknowledgment appears in derived source files. The citation
* should list that the code comes from the book "Linux Device
* Drivers" by Alessandro Rubini and Jonathan Corbet, published
* by O'Reilly & Associates. No warranty is attached;
* we cannot take responsibility for errors or fitness for use.
*
* $Id: shortprint.c,v 1.4 2004/09/26 08:01:04 gregkh Exp $
*/
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/sched.h>
#include <linux/sched/signal.h>
#include <linux/kernel.h> /* printk() */
#include <linux/fs.h> /* everything... */
#include <linux/errno.h> /* error codes */
#include <linux/delay.h> /* udelay */
#include <linux/slab.h>
#include <linux/ioport.h>
#include <linux/interrupt.h>
#include <linux/workqueue.h>
#include <linux/timer.h>
#include <linux/poll.h>
#include <asm/io.h>
#include <linux/semaphore.h>
#include <asm/atomic.h>
#include "shortprint.h"
#define SHORTP_NR_PORTS 3
/*
* all of the parameters have no "shortp_" prefix, to save typing when
* specifying them at load time
*/
static int major = 0; /* dynamic by default */
module_param(major, int, 0);
/* default is the first printer port on PC's. "shortp_base" is there too
because it's what we want to use in the code */
static unsigned long base = 0x378;
unsigned long shortp_base = 0;
module_param(base, long, 0);
/* The interrupt line is undefined by default. "shortp_irq" is as above */
static int irq = -1;
static int shortp_irq = -1;
module_param(irq, int, 0);
/* Microsecond delay around strobe. */
static int delay = 0;
static int shortp_delay;
module_param(delay, int, 0);
MODULE_AUTHOR ("Jonathan Corbet");
MODULE_LICENSE("Dual BSD/GPL");
/*
* Forwards.
*/
static void shortp_cleanup(void);
static void shortp_timeout(struct timer_list *unused);
/*
* Input is managed through a simple circular buffer which, among other things,
* is allowed to overrun if the reader isn't fast enough. That makes life simple
* on the "read" interrupt side, where we don't want to block.
*/
static unsigned long shortp_in_buffer = 0;
static unsigned long volatile shortp_in_head;
static volatile unsigned long shortp_in_tail;
DECLARE_WAIT_QUEUE_HEAD(shortp_in_queue);
static struct timespec64 shortp_tv; /* When the interrupt happened. */
/*
* Atomicly increment an index into shortp_in_buffer
*/
static inline void shortp_incr_bp(volatile unsigned long *index, int delta)
{
unsigned long new = *index + delta;
barrier (); /* Don't optimize these two together */
*index = (new >= (shortp_in_buffer + PAGE_SIZE)) ? shortp_in_buffer : new;
}
/*
* On the write side we have to be more careful, since we don't want to drop
* data. The semaphore is used to serialize write-side access to the buffer;
* there is only one consumer, so read-side access is unregulated. The
* wait queue will be awakened when space becomes available in the buffer.
*/
static unsigned char *shortp_out_buffer = NULL;
static volatile unsigned char *shortp_out_head, *shortp_out_tail;
static struct mutex shortp_out_mutex;
static DECLARE_WAIT_QUEUE_HEAD(shortp_out_queue);
/*
* Feeding the output queue to the device is handled by way of a
* workqueue.
*/
static void shortp_do_work(struct work_struct *work);
static DECLARE_WORK(shortp_work, shortp_do_work);
static struct workqueue_struct *shortp_workqueue;
/*
* Available space in the output buffer; should be called with the semaphore
* held. Returns contiguous space, so caller need not worry about wraps.
*/
static inline int shortp_out_space(void)
{
if (shortp_out_head >= shortp_out_tail) {
int space = PAGE_SIZE - (shortp_out_head - shortp_out_buffer);
return (shortp_out_tail == shortp_out_buffer) ? space - 1 : space;
} else
return (shortp_out_tail - shortp_out_head) - 1;
}
static inline void shortp_incr_out_bp(volatile unsigned char **bp, int incr)
{
unsigned char *new = (unsigned char *) *bp + incr;
if (new >= (shortp_out_buffer + PAGE_SIZE))
new -= PAGE_SIZE;
*bp = new;
}
/*
* The output "process" is controlled by a spin lock; decisions on
* shortp_output_active or manipulation of shortp_out_tail require
* that this lock be held.
*/
static spinlock_t shortp_out_lock;
volatile static int shortp_output_active;
DECLARE_WAIT_QUEUE_HEAD(shortp_empty_queue); /* waked when queue empties */
/*
* When output is active, the timer is too, in case we miss interrupts. Hold
* shortp_out_lock if you mess with the timer.
*/
static struct timer_list shortp_timer;
#define TIMEOUT 5*HZ /* Wait a long time */
/*
* Open the device.
*/
static int shortp_open(struct inode *inode, struct file *filp)
{
return 0;
}
static int shortp_release(struct inode *inode, struct file *filp)
{
/* Wait for any pending output to complete */
wait_event_interruptible(shortp_empty_queue, shortp_output_active==0);
return 0;
}
static unsigned int shortp_poll(struct file *filp, poll_table *wait)
{
return POLLIN | POLLRDNORM | POLLOUT | POLLWRNORM;
}
/*
* The read routine, which doesn't return data from the device; instead, it
* returns timing information just like the "short" device.
*/
static ssize_t shortp_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
int count0;
DEFINE_WAIT(wait);
while (shortp_in_head == shortp_in_tail) {
prepare_to_wait(&shortp_in_queue, &wait, TASK_INTERRUPTIBLE);
if (shortp_in_head == shortp_in_tail)
schedule();
finish_wait(&shortp_in_queue, &wait);
if (signal_pending (current)) /* a signal arrived */
return -ERESTARTSYS; /* tell the fs layer to handle it */
}
/* count0 is the number of readable data bytes */
count0 = shortp_in_head - shortp_in_tail;
if (count0 < 0) /* wrapped */
count0 = shortp_in_buffer + PAGE_SIZE - shortp_in_tail;
if (count0 < count)
count = count0;
if (copy_to_user(buf, (char *)shortp_in_tail, count))
return -EFAULT;
shortp_incr_bp(&shortp_in_tail, count);
return count;
}
/*
* Wait for the printer to be ready; this can sleep.
*/
static void shortp_wait(void)
{
if ((inb(shortp_base + SP_STATUS) & SP_SR_BUSY) == 0) {
printk(KERN_INFO "shortprint: waiting for printer busy\n");
printk(KERN_INFO "Status is 0x%x\n", inb(shortp_base + SP_STATUS));
while ((inb(shortp_base + SP_STATUS) & SP_SR_BUSY) == 0) {
set_current_state(TASK_INTERRUPTIBLE);
schedule_timeout(10*HZ);
}
}
}
/*
* Write the next character from the buffer. There should *be* a next
* character... The spinlock should be held when this routine is called.
*/
static void shortp_do_write(void)
{
unsigned char cr = inb(shortp_base + SP_CONTROL);
/* Something happened; reset the timer */
mod_timer(&shortp_timer, jiffies + TIMEOUT);
/* Strobe a byte out to the device */
outb_p(*shortp_out_tail, shortp_base+SP_DATA);
shortp_incr_out_bp(&shortp_out_tail, 1);
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);
}
/*
* Start output; call under lock.
*/
static void shortp_start_output(void)
{
if (shortp_output_active) /* Should never happen */
return;
/* Set up our 'missed interrupt' timer */
shortp_output_active = 1;
shortp_timer.expires = jiffies + TIMEOUT;
add_timer(&shortp_timer);
/* And get the process going. */
queue_work(shortp_workqueue, &shortp_work);
}
/*
* Write to the device.
*/
static ssize_t shortp_write(struct file *filp, const char __user *buf, size_t count,
loff_t *f_pos)
{
int space, written = 0;
unsigned long flags;
/*
* Take and hold the mutex for the entire duration of the operation. The
* consumer side ignores it, and it will keep other data from interleaving
* with ours.
*/
if (mutex_lock_interruptible(&shortp_out_mutex))
return -ERESTARTSYS;
/*
* Out with the data.
*/
while (written < count) {
/* Hang out until some buffer space is available. */
space = shortp_out_space();
if (space <= 0) {
if (wait_event_interruptible(shortp_out_queue,
(space = shortp_out_space()) > 0))
goto out;
}
/* Move data into the buffer. */
if ((space + written) > count)
space = count - written;
if (copy_from_user((char *) shortp_out_head, buf, space)) {
mutex_unlock(&shortp_out_mutex);
return -EFAULT;
}
shortp_incr_out_bp(&shortp_out_head, space);
buf += space;
written += space;
/* If no output is active, make it active. */
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;
mutex_unlock(&shortp_out_mutex);
return written;
}
/*
* The bottom-half handler.
*/
static void shortp_do_work(struct work_struct *work)
{
int written;
unsigned long flags;
/* Wait until the device is ready */
shortp_wait();
spin_lock_irqsave(&shortp_out_lock, flags);
/* Have we written everything? */
if (shortp_out_head == shortp_out_tail) { /* empty */
shortp_output_active = 0;
wake_up_interruptible(&shortp_empty_queue);
del_timer(&shortp_timer);
}
/* Nope, write another byte */
else
shortp_do_write();
/* If somebody's waiting, maybe wake them up. */
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);
/* Handle the "read" side operation */
written = sprintf((char *)shortp_in_head, "%08u.%09u\n",
(int)(shortp_tv.tv_sec % 100000000),
(int)(shortp_tv.tv_nsec));
shortp_incr_bp(&shortp_in_head, written);
wake_up_interruptible(&shortp_in_queue); /* awake any reading process */
}
/*
* The top-half interrupt handler.
*/
static irqreturn_t shortp_interrupt(int irq, void *dev_id)
{
if (! shortp_output_active)
return IRQ_NONE;
/* Remember the time, and farm off the rest to the workqueue function */
ktime_get_real_ts64(&shortp_tv);
queue_work(shortp_workqueue, &shortp_work);
return IRQ_HANDLED;
}
/*
* Interrupt timeouts. Just because we got a timeout doesn't mean that
* things have gone wrong, however; printers can spend an awful long time
* just thinking about things.
*/
static void shortp_timeout(struct timer_list *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 the printer is still busy we just reset the timer */
if ((status & SP_SR_BUSY) == 0 || (status & SP_SR_ACK)) {
shortp_timer.expires = jiffies + TIMEOUT;
add_timer(&shortp_timer);
spin_unlock_irqrestore(&shortp_out_lock, flags);
return;
}
/* Otherwise we must have dropped an interrupt. */
spin_unlock_irqrestore(&shortp_out_lock, flags);
shortp_interrupt(shortp_irq, NULL);
}
static struct file_operations shortp_fops = {
.read = shortp_read,
.write = shortp_write,
.open = shortp_open,
.release = shortp_release,
.poll = shortp_poll,
.owner = THIS_MODULE
};
/*
* Module initialization
*/
static int shortp_init(void)
{
int result;
/*
* first, sort out the base/shortp_base ambiguity: we'd better
* use shortp_base in the code, for clarity, but allow setting
* just "base" at load time. Same for "irq".
*/
shortp_base = base;
shortp_irq = irq;
shortp_delay = delay;
/* Get our needed resources. */
if (! request_region(shortp_base, SHORTP_NR_PORTS, "shortprint")) {
printk(KERN_INFO "shortprint: can't get I/O port address 0x%lx\n",
shortp_base);
return -ENODEV;
}
/* Register the device */
result = register_chrdev(major, "shortprint", &shortp_fops);
if (result < 0) {
printk(KERN_INFO "shortp: can't get major number\n");
release_region(shortp_base, SHORTP_NR_PORTS);
return result;
}
if (major == 0)
major = result; /* dynamic */
/* Initialize the input buffer. */
shortp_in_buffer = __get_free_pages(GFP_KERNEL, 0); /* never fails */
shortp_in_head = shortp_in_tail = shortp_in_buffer;
/* And the output buffer. */
shortp_out_buffer = (unsigned char *) __get_free_pages(GFP_KERNEL, 0);
shortp_out_head = shortp_out_tail = shortp_out_buffer;
mutex_init(&shortp_out_mutex);
/* And the output info */
shortp_output_active = 0;
spin_lock_init(&shortp_out_lock);
timer_setup(&shortp_timer, shortp_timeout, 0);
/* Set up our workqueue. */
shortp_workqueue = create_singlethread_workqueue("shortprint");
/* If no IRQ was explicitly requested, pick a default */
if (shortp_irq < 0)
switch(shortp_base) {
case 0x378: shortp_irq = 7; break;
case 0x278: shortp_irq = 2; break;
case 0x3bc: shortp_irq = 5; break;
}
/* Request the IRQ */
result = request_irq(shortp_irq, shortp_interrupt, 0, "shortprint", NULL);
if (result) {
printk(KERN_INFO "shortprint: can't get assigned irq %i\n",
shortp_irq);
shortp_irq = -1;
shortp_cleanup ();
return result;
}
/* Initialize the control register, turning on interrupts. */
outb(SP_CR_IRQ | SP_CR_SELECT | SP_CR_INIT, shortp_base + SP_CONTROL);
return 0;
}
static void shortp_cleanup(void)
{
/* Return the IRQ if we have one */
if (shortp_irq >= 0) {
outb(0x0, shortp_base + SP_CONTROL); /* disable the interrupt */
free_irq(shortp_irq, NULL);
}
/* All done with the device */
unregister_chrdev(major, "shortprint");
release_region(shortp_base,SHORTP_NR_PORTS);
/* Don't leave any timers floating around. Note that any active output
is effectively stopped by turning off the interrupt */
if (shortp_output_active)
del_timer_sync (&shortp_timer);
flush_workqueue(shortp_workqueue);
destroy_workqueue(shortp_workqueue);
if (shortp_in_buffer)
free_page(shortp_in_buffer);
if (shortp_out_buffer)
free_page((unsigned long) shortp_out_buffer);
}
module_init(shortp_init);
module_exit(shortp_cleanup);