【Linux 驱动】中断处理(一)上半部

关于中断大家应该很熟悉,无论是操作系统的学习也好还是微机原理也好都会接触到中断。以前进行单片机单板开发的时候,会有外部中断,定时器中断以及串口中断,就是有一个事件触发(外部中断则是外部有按键触发(边缘触发和水平触发),串口中断则是串口有数据输入,定时器中断则是内部设置一个定时器,时间到了就触发),然后就去调用对应的中断处理函数。在此之前,需要保护现场,保存中断发生时的状态,比如寄存器,堆栈,各种变量等等,一旦中断程序执行完毕,程序又会回到当初中断发生的地方重新执行。

概念性的东西就不费笔墨了。
当然,linux内核的中断当然要比上面复杂的多,上面只是大致阐述下中断的概念。

具体到一个操作系统,需要对连接到计算机上的硬件设备进行有效管理,就需要硬件对操作系统做出响应(不要问我为什么不是操作系统去向硬件发出请求)。就是让硬件在需要的时候向内核发出信号,这就是中断机制。

不同的设备对应的中断不同,而每个中断都通过一个唯一的数字标志。(参见大学微机原理课程)这样操作系统就知道是谁发出的中断请求,从而给不同的中断提供对应的中断处理程序。

先看代码:

#include <linux/module.h>
#include <linux/types.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/wait.h>
#include <linux/sched.h>
#include <linux/errno.h>
#include <asm/uaccess.h>
#include <linux/poll.h>
#include <linux/semaphore.h>
#include <linux/fcntl.h>
#include <linux/interrupt.h>

MODULE_LICENSE("Dual BSD/GPL");

#define DEV_SIZE 20
#define WQ_MAJOR 230

#define DEBUG_SWITCH 1
#if DEBUG_SWITCH
    #define P_DEBUG(fmt, args...) printk("<1>" "<kernel>[%s]"fmt,__FUNCTION__, ##args)
#else
    #define P_DEBUG(fmt, args...) printk("<7>" "<kernel>[%s]"fmt,__FUNCTION__, ##args)
#endif

static int irq;
static char *devname;

//模块参数,允许在运行insmod加载模块的时候对下面参数赋值
module_param(irq, int, S_IRUGO);
module_param(devname, charp, S_IRUGO);

struct wq_dev{
    char kbuf[DEV_SIZE];//缓冲区
    dev_t devno;//设备号
    unsigned int major;
    struct cdev wq_cdev;
    unsigned int cur_size;//可读可写的数据量
    struct semaphore sem;//信号量
    wait_queue_head_t r_wait;//读等待队列
    wait_queue_head_t w_wait;//写等待队列
    struct fasync_struct *async_queue;//异步通知队列 
};

//struct wq_dev *wq_devp;

//异步通知机制驱动函数
static int wq_fasync(int fd, struct file *filp, int mode)
{
    struct wq_dev *dev = filp->private_data;
    return fasync_helper(fd, filp, mode, &dev->async_queue);//调用内核提供的函数
}

//中断处理程序
static irqreturn_t wq_irq_handler(int irq, void *dev)
{
    struct wq_dev mydev;
    static int count = 1;
    mydev = *(struct wq_dev*)dev;
    printk("key:%d\n",count);
    printk("ISR is working...\n");
    count++;
    return IRQ_HANDLED;
}

int wq_open(struct inode *inodep, struct file *filp)
{
    struct wq_dev *dev;
    dev = container_of(inodep->i_cdev, struct wq_dev, wq_cdev);
    filp->private_data = dev;

    //文件打开的时候,注册中断处理程序,激活给定的中断线
    if(request_irq(irq, wq_irq_handler, IRQF_SHARED, devname, dev) != 0)
    {
        printk("%s request IRQ:%d failed...\n", devname, irq);
        return -1;
    }
    printk("%s request IRQ:%d sucess...\n", devname, irq);

    printk(KERN_ALERT "open is ok!\n");
    return 0;
}

int wq_release(struct inode *inodep, struct file *filp)
{
    printk(KERN_ALERT "release is ok!\n");
    wq_fasync(-1, filp, 0);//从异步通知队列中删除该filp
    return 0;
}

static ssize_t wq_read(struct file *filp, char __user *buf, size_t count, loff_t *offset)
{
    struct wq_dev *dev = filp->private_data;

    P_DEBUG("read data...\n");

    if(down_interruptible(&dev->sem))//获取信号量
    {
        P_DEBUG("enter read down_interruptible\n");
        return -ERESTARTSYS;
    }
    P_DEBUG("read first down\n");
    while(dev->cur_size == 0){//无数据可读,进入休眠lon
        up(&dev->sem);//释放信号量,不然写进程没有机会来唤醒(没有获得锁)
        if(filp->f_flags & O_NONBLOCK)//检查是否是阻塞型I/O
            return -EAGAIN;
        P_DEBUG("%s reading:going to sleep\n", current->comm);
        if(wait_event_interruptible(dev->r_wait, dev->cur_size != 0))//休眠等待被唤醒
        {
            P_DEBUG("read wait interruptible\n");
            return -ERESTARTSYS;
        }
        P_DEBUG("wake up r_wait\n");
        if(down_interruptible(&dev->sem))//获取信号量
            return -ERESTARTSYS;
    }

    //数据已就绪
    P_DEBUG("[2]dev->cur_size is %d\n", dev->cur_size);
    if(dev->cur_size > 0)
        count = min(count, dev->cur_size);

    //从内核缓冲区赋值数据到用户空间,复制成功返回0
    if(copy_to_user(buf, dev->kbuf, count))
    {
        up(&dev->sem);
        return -EFAULT;
    }   
    dev->cur_size -= count;//可读数据量更新
    up(&dev->sem);
    wake_up_interruptible(&dev->w_wait);//唤醒写进程
    P_DEBUG("%s did read %d bytes\n", current->comm, (unsigned int)count);
    return count;
}

static ssize_t wq_write(struct file *filp,const char __user *buf,size_t count, loff_t *offset)
{
    struct wq_dev *dev = filp->private_data;
    //wait_queue_t my_wait;
    P_DEBUG("write is doing\n");    
    if(down_interruptible(&dev->sem))//获取信号量
    {
        P_DEBUG("enter write down_interruptible\n");
        return -ERESTARTSYS;
    }

    P_DEBUG("write first down\n");
    while(dev->cur_size == DEV_SIZE){//判断空间是否已满

        up(&dev->sem);//释放信号量
        if(filp->f_flags & O_NONBLOCK)
            return -EAGAIN;
        P_DEBUG("writing going to sleep\n");
        if(wait_event_interruptible(dev->w_wait, dev->cur_size < DEV_SIZE))
            return -ERESTARTSYS;

        if(down_interruptible(&dev->sem))//获取信号量
            return -ERESTARTSYS;
    }
    if(count > DEV_SIZE - dev->cur_size)
        count = DEV_SIZE - dev->cur_size;

    if(copy_from_user(dev->kbuf, buf, count))//数据复制
        return -EFAULT;
    dev->cur_size += count;//更新数据量
    P_DEBUG("write %d bytes , cur_size:[%d]\n", count, dev->cur_size);
    P_DEBUG("kbuf is [%s]\n", dev->kbuf);
    up(&dev->sem);
    wake_up_interruptible(&dev->r_wait);//唤醒读进程队列

    if(dev->async_queue)
        kill_fasync(&dev->async_queue, SIGIO, POLL_IN);//可写时发送信号

    return count;
}

static unsigned int wq_poll(struct file *filp, poll_table *wait)
{
    struct wq_dev *dev = filp->private_data;
    unsigned int mask = 0;

    if(down_interruptible(&dev->sem))//获取信号量
        return -ERESTARTSYS;
    poll_wait(filp, &dev->w_wait, wait);//添加写等待队列
    poll_wait(filp, &dev->r_wait, wait);//添加读等待队列

    if(dev->cur_size != 0)//判断是否可读取
        mask |= POLLIN | POLLRDNORM;
    if(dev->cur_size != DEV_SIZE)//判断是否可写入
        mask |= POLLOUT | POLLWRNORM;

    up(&dev->sem);//释放信号量
    return mask;
}
struct file_operations wq_fops = {
    .open = wq_open,
    .release = wq_release,
    .write = wq_write,
    .read = wq_read,
    .poll = wq_poll,
    .fasync = wq_fasync,//函数注册
};

struct wq_dev my_dev;

static int __init wq_init(void)
{
    int result = 0;
    my_dev.cur_size = 0;
    my_dev.devno = MKDEV(WQ_MAJOR, 0);
    //设备号分配
    if(WQ_MAJOR)
        result = register_chrdev_region(my_dev.devno, 1, "wqlkp");
    else
    {
        result = alloc_chrdev_region(&my_dev.devno, 0, 1, "wqlkp");
        my_dev.major = MAJOR(my_dev.devno);
    }
    if(result < 0)
        return result;

    cdev_init(&my_dev.wq_cdev, &wq_fops);//设备初始化
    my_dev.wq_cdev.owner = THIS_MODULE;
    sema_init(&my_dev.sem, 1);//信号量初始化
    init_waitqueue_head(&my_dev.r_wait);//等待队列初始化
    init_waitqueue_head(&my_dev.w_wait);

    result = cdev_add(&my_dev.wq_cdev, my_dev.devno, 1);//设备注册
    if(result < 0)
    {
        P_DEBUG("cdev_add error!\n");
        goto err;
    }
    printk(KERN_ALERT "hello kernel\n");
    return 0;

err:
    unregister_chrdev_region(my_dev.devno,1);
    return result;
}

static void __exit wq_exit(void)
{
    cdev_del(&my_dev.wq_cdev);
    unregister_chrdev_region(my_dev.devno, 1);
    free_irq(irq, &my_dev);//卸载驱动的时候,注销相应的中断处理程序,并释放中断线(其释放机制类似于C++中的引用,linux中的描述符的引用...) 
}

module_init(wq_init);
module_exit(wq_exit);

编译后,insmod wqlko.ko irq=1 devname=myirq
然后./app_read(./app_write)。
dmesg看打印信息。
cat /proc/interrupts 查看共享的中断线

尤其这里指定irq=1,是与键盘共享同一根中断线,所以一旦键盘发出中断请求,系统响应后也会执行我们的中断处理程序。

上面只是一个简单的中断处理程序样本,还没有涉及到下半部tasklet等,也不涉及到具体硬件(直接在PC下)。
主要注册中断处理程序和释放中断处理程序。

调用request_irq的正确位置应该是在设备第一次打开,硬件被告知产生中断之前;
调用free_irq的位置是最后一次关闭设备,硬件被告知不再使用中断处理器之后。

对于request_irq函数,内核维护了一个中断信号线的注册表,模块使用中断之前要先请求一个中断通道(或者中断请求IRQ),然后在使用后释放该通道。
我们这里是和其他驱动程序共享中断信号线。

同样的,我们这里依旧跟踪下linux 内核源码,不过与前面不同的是
中断处理系统在linux中的实现是非常依赖体系结构的,实现依赖于处理器,所使用的终端处理器的类型,体系结构的设计及机器本身。

设备产生中断,通过总线把电信号发送给中断控制器,如果中断是激活的(没被屏蔽),那么中断控制器就会把中断发往处理器,如果处理器没禁止该中断,处理器会停止当前的事,跳到内存中预定义的位置开始执行哪里的代码。这个预定义的位置是由内核设置的,是中断处理程序的入口处。

【Linux 驱动】中断处理(一)上半部_第1张图片
在内核中,中断的旅程开始于预定义入口点,这类似于系统调用通过预定义的异常句柄进入内核。对于每条中断线,处理器都会跳到对应的唯一的位置,这样内核就知道所接收中断的IRQ号了。初始入口点只是在栈中保存这个号,并存放当前寄存器的只;然后,内核调用函数do_IRQ。

先看request_irq函数:

/**其实看下面的英文注释就知道这个函数的功能了 * request_irq - allocate an interrupt line * @irq: Interrupt line to allocate * @handler: Function to be called when the IRQ occurs * @irqflags: Interrupt type flags * @devname: An ascii name for the claiming device * @dev_id: A cookie passed back to the handler function * * This call allocates interrupt resources and enables the * interrupt line and IRQ handling. From the point this * call is made your handler function may be invoked. Since * your handler function must clear any interrupt the board * raises, you must take care both to initialise your hardware * and to set up the interrupt handler in the right order. * * Dev_id must be globally unique. Normally the address of the * device data structure is used as the cookie. Since the handler * receives this value it makes sense to use it. * 这里写到Dev_id必须是全局唯一的变量,因为对于共享的中断线,需要一个唯一的信息 * 来区分其上面的多个处理程序,并让free_irq()仅仅删除指定的处理程序。 * * If your interrupt is shared you must pass a non NULL dev_id * as this is required when freeing the interrupt. * * Flags: * * IRQF_SHARED Interrupt is shared * * IRQF_DISABLED Disable local interrupts while processing * * IRQF_SAMPLE_RANDOM The interrupt can be used for entropy * */

//FIXME - handler used to return void - whats the significance of the change?
int request_irq(unsigned int irq, irqreturn_t (*handler)(int, void *, struct pt_regs *),
         unsigned long irq_flags, const char * devname, void *dev_id)
{
    unsigned long retval;
    struct irqaction *action;//irqaction结构体

    if (irq >= NR_IRQS || !irq_desc[irq].valid || !handler ||
        (irq_flags & IRQF_SHARED && !dev_id))
        return -EINVAL;

    action = (struct irqaction *)kmalloc(sizeof(struct irqaction), GFP_KERNEL);//新建一个irqaction结构体
    if (!action)
        return -ENOMEM;

    action->handler = handler;//中断处理程序
    action->flags = irq_flags;//标识
    cpus_clear(action->mask);
    action->name = devname;//设备名
    action->next = NULL;
    action->dev_id = dev_id;//标识设备本身

    retval = setup_irq(irq, action);//调用setup_irq注册中断处理程序

    if (retval)
        kfree(action);
    return retval;
}

/* struct irqaction { irqreturn_t (*handler)(int, void *, struct pt_regs *);//中断处理程序 unsigned long flags;//标志 cpumask_t mask;//未使用 const char *name;//设备名 void *dev_id;//标识设备本身 struct irqaction *next;//下一个元素,链表中的元素指向共享同一IRQ的硬件设备 int irq;//IRQ线 struct proc_dir_entry *dir;//指向与TRQn相关的/proc/irq/n目录的描述符 }; */

//下面相继就是调用setup_irq();
struct irq_desc *desc = irq_desc + irq;//该语句就是根据中断号搜索到对应的中断描述符结构。
//下面的不追溯了,就是负责整个的注册事宜。

不管引起中断的电路种类如何,所有的I/O中断处理程序都执行四个相通的基本步骤:

在内核态堆栈中保存IRQ的值和寄存器的内容;
为正在给IRQ线服务的PIC发送一个应答,这将允许PIC进一步发出中断;
执行共享这个IRQ的所有设备的中断服务里程(ISR);
跳到ret_from_intr()的地址后终止(这部分是硬件控制的)。
ok,看do_IRQ()

/* * do_IRQ handles all normal device IRQ's (the special * SMP cross-CPU interrupts have their own specific * handlers). */
fastcall unsigned int do_IRQ(struct pt_regs *regs)
{   
    /* high bit used in ret_from_ code */
    int irq = ~regs->orig_eax;//保存IRQ值
    ……
    //这部分代码略,请参考arch/i386/kernel/Irq.c 
    //保存寄存器的内容
    ……
        __do_IRQ(irq, regs);

    irq_exit();

    return 1;
}

/** * __do_IRQ - original all in one highlevel IRQ handler * @irq: the interrupt number * @regs: pointer to a register structure * * __do_IRQ handles all normal device IRQ's (the special * SMP cross-CPU interrupts have their own specific * handlers). * * This is the original x86 implementation which is used for every * interrupt type. */
 //上面的英文注释已经大致解释了一番...如果英文好,这条路会好走很多
fastcall unsigned int __do_IRQ(unsigned int irq, struct pt_regs *regs)
{
    struct irq_desc *desc = irq_desc + irq;//找到中断描述符
    struct irqaction *action;
    unsigned int status;

    kstat_this_cpu.irqs[irq]++;
    if (CHECK_IRQ_PER_CPU(desc->status)) {//该线上有中断处理程序
        irqreturn_t action_ret;

        /* * No locking required for CPU-local interrupts: */
        if (desc->chip->ack)
            desc->chip->ack(irq);
        action_ret = handle_IRQ_event(irq, regs, desc->action);//转调用该函数
        desc->chip->end(irq);//结束中断
        return 1;
    }
    ……
}

irqreturn_t handle_IRQ_event(unsigned int irq, struct pt_regs *regs,
                 struct irqaction *action)
{
    irqreturn_t ret, retval = IRQ_NONE;
    unsigned int status = 0;

    handle_dynamic_tick(action);

    if (!(action->flags & IRQF_DISABLED))
        local_irq_enable_in_hardirq();

//下面通过循环,调用同一共享中断线上的所有中断服务程序
//这也就告诉我们上面的例程与键盘共享同一信号线,一旦键盘有中断请求,处理器响应之后,
//也会调用我们自定的中断服务程序
    do {
        ret = action->handler(irq, action->dev_id, regs);//调用我们前面注册的中断服务程序
        if (ret == IRQ_HANDLED)
            status |= action->flags;
        retval |= ret;
        action = action->next;//链表的下一个元素,也就是共享同一中断线
    } while (action);

    if (status & IRQF_SAMPLE_RANDOM)
        add_interrupt_randomness(irq);
    local_irq_disable();

    return retval;
}

中断返回以后,处理器会控制执行ret_from_intr(),返回内核执行中断的代码。
2
限于篇幅,未完,待续…

你可能感兴趣的:(Linux驱动-中断)