Linux设备驱动中断基础知识

1、基本概念

在Linux系统中,中断服务程序的执行是与一般的进程异步的,也就是不存在于进程上下文,要求中断服务程序的执行时间尽可能短,因此,Linux系统在中断处理中引入了顶半部和低半部的分离机制。

 

2、Linux中断及中断处理架构

设备中断的到来会打断Linux内核中进程的正常调度和运行,对于系统更高吞吐率的追求导致要求中断服务程序尽可能的短小精悍,但是,在大多数真实的系统中,中断产生后,需要完成的工作可能要进行较大量的耗时处理。

为了能在中断执行时间尽可能短和中断处理需要完成大量工作之间找到一个平衡点,Linux系统将中断处理程序分解成两个部分,分别是:

  • 顶半部(top half)
  • 低半部(bottom half)

顶半部完成尽可能少的比较紧急的功能,它往往只是简单地读取寄存器中的中断状态,并清除中断标志后就进行“登记中断”的工作,“登记中断”就是指将低半部处理程序挂到设备的低半部执行队列中去,这样操作后,顶半部执行的速度就会很快,从而使得CPU能够服务更多的中断请求。

中断处理的工作重心也就落到了低半部中去,由它来完成中断事件的绝大部分任务,而且可以被新的中断打断,这也是低半部和顶半部最大的不同,顶半部往往被设计成不可中断,低半部相对来说并不是非常紧急,而且相对比较耗时,因此,不适合在硬件中断服务程序中执行。

尽管顶半部、低半部的分离机制能够改善系统的响应能力,但是,由此认为Linux设备驱动中的中断处理一定要分为两个半部去处理则是不恰当的,当遇到中断处理的工作很少的场景时,完全可以直接在顶半部全部处理完成。

在Linux系统中,可以查看/proc/interrupts文件可以获取到系统中断的统计信息,可以直接使用下面命令查看:

# cat /proc/interrupts

 

3、Linux中断编程

中断处理与进程是CPU上两类完全独立的执行体,因此,它们具有两类上下文,关于这个的理解,可以参考下面的文章,链接如下:

https://www.cnblogs.com/Cqlismy/p/14433335.html

(3.1)申请和释放中断

在Linux设备驱动中,想要使用中断的设备需要申请和释放相对应的中断,分别使用Linux内核提供的request_irq()和free_irq()函数接口。

(3.1.1)申请IRQ

如果想要使用中断的话,首先需要申请对应的中断资源,在硬件上就体现为关联上了某条中断线,request_irq()函数接口如下所示:

static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
        const char *name, void *dev)
{
    return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}

关于该函数的一些参数说明如下:

  • irq参数是要申请的硬件中断号;
  • handler参数是一个函数指针,是向系统登记的中断处理函数,是一个回调函数,当中断发生时,系统就会调用这个函数;
  • flags参数是中断类型的标志,常用的标志有IRQF_SHARED,表示中断是共享的,还有IRQF_TRIGGER_*,表示中断的触发类型,例如:上升沿触发、下降沿触发、双边沿触发等;
  • name参数是申请中断的设备的字符串名称;
  • dev参数是中断发生后,用来传递给handler函数的,一般设置为设备的设备结构体或者NULL。

该函数调用后返回0,表示申请中断成功,如果返回的是负的错误号,表示申请中断失败,在编写程序时,必须对该函数的返回值进行判断,以确保想使用的中断资源是否已经申请成功。

(3.1.2)释放IRQ

如果想要释放掉已经申请的中断资源的话,可以使用free_irq()函数,它是和request_irq()函数功能相反的,该函数的实现如下:

void free_irq(unsigned int irq, void *dev_id)
{
    struct irq_desc *desc = irq_to_desc(irq);

    if (!desc || WARN_ON(irq_settings_is_per_cpu_devid(desc)))
        return;

#ifdef CONFIG_SMP
    if (WARN_ON(desc->affinity_notify))
        desc->affinity_notify = NULL;
#endif

    kfree(__free_irq(irq, dev_id));
}

关于该函数参数的一些说明如下:

  • irq参数是已经申请的硬件中断号;
  • dev_id参数和request_irq()函数的dev参数对应,一般为设备的设备结构体或者NULL。

(3.2)使能和屏蔽中断

在Linux设备驱动中断编程中,如果想要使能或者屏蔽中断的话,可以使用enable_irq()和disable_irq()内核函数接口。

(3.2.1)使能中断IRQ

使能中断IRQ,可以使用enable_irq()函数接口,该函数的定义如下:

void enable_irq(unsigned int irq)
{
    unsigned long flags;
    struct irq_desc *desc = irq_get_desc_buslock(irq, &flags, IRQ_GET_DESC_CHECK_GLOBAL);

    if (!desc)
        return;
    if (WARN(!desc->irq_data.chip,
         KERN_ERR "enable_irq before setup/request_irq: irq %u\n", irq))
        goto out;

    __enable_irq(desc);
out:
    irq_put_desc_busunlock(desc, flags);
}

该函数的形参只有一个,就是irq参数,该参数是要使能中断的硬件中断号。

(3.2.2)屏蔽中断IRQ

屏蔽中断IRQ,可以使用disable_irq()函数和disable_irq_nosync()函数,这两个函数的定义如下:

void disable_irq(unsigned int irq)
{
    if (!__disable_irq_nosync(irq))
        synchronize_irq(irq);
}

void disable_irq_nosync(unsigned int irq)
{
    __disable_irq_nosync(irq);
}

两个函数的形参都是irq,表示要屏蔽中断的硬件中断号,这两个函数的区别在于,disable_irq()函数会等待目前的中断处理完成,而disable_irq_nosync()函数则不会等待。

(3.3)低半部常用机制

Linux中断处理程序分成了上下两个部分,低半部的实现机制主要有tasklet、工作队列和软中断,接下来了解一下这些低半部机制如何使用。

(3.3.1)tasklet

在Linux内核源码中,tasklet由struct tasklet_struct结构体来表示,每个结构体代表一个tasklet,该结构体的描述如下:

struct tasklet_struct
{
    struct tasklet_struct *next;
    unsigned long state;
    atomic_t count;
    void (*func)(unsigned long);
    unsigned long data;
};

该结构体中的成员作用如下:

  • next:用来指向下一个tasklet_struct结构体,也是就下一个tasklet,说明tasklet会被加入到一个链表中;
  • state:用来标识tasklet的状态,一个无符号长整数,当前只使用了bit[1]和bit[0]两个状态位,其中bit[1]=1时,表示这个tasklet当前正在某个CPU上执行,其在SMP系统上才有意义,目的是为了防止多个CPU同时执行一个tasklet的情况出现,bit[0]=1时表示tasklet已经被调度等待执行了,但是还没有执行,其目的是为了防止同一个tasklet在被运行之前,重复调度;
  • count:引用计数,若该值不为0时,tasklet被禁止,若该值为0时,表示tasklet已经被激活,tasklet调度后才能够执行;
  • func:函数指针,也就是回调函数,对应这个tasklet的处理函数;
  • data:一个无符号长整数,func的参数,其具体含义由func函数自行解释,比如可以将其解释成用户自己定义的数据结构体的指针。

tasklet的使用比较简单,我们只需要定义tasklet以及它的处理函数,将两者相关联,在需要调度tasklet的时候引用tasklet_schedule()函数,内核系统就会在适当的时候进行调度指定的tasklet去运行。

对于tasklet的定义可以使用下面两个宏,代码如下:

#define DECLARE_TASKLET(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }

#define DECLARE_TASKLET_DISABLED(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }

两个宏之间的区别在于引用计数的初始值设置不同,DECLARE_TASKLET宏定义将引用计数的初始值设置为0,表示tasklet一开始就处于激活状态,可以直接被调度,而DECLARE_TASKLET_DISABLED宏定义将引用计数的初始化值设置为1,表示tasklet一开始处于非激活状态。

对于tasklet的使能和禁止,可以使用tasklet_enable()函数和tasklet_disable()函数,这两个函数的定义如下:

static inline void tasklet_disable(struct tasklet_struct *t)
{
    tasklet_disable_nosync(t);
    tasklet_unlock_wait(t);
    smp_mb();
}

static inline void tasklet_enable(struct tasklet_struct *t)
{
    smp_mb__before_atomic();
    atomic_dec(&t->count);
}

如果想要调度自己定义的tasklet的话,可以使用tasklet_schedule()函数,该函数的定义如下:

static inline void tasklet_schedule(struct tasklet_struct *t)
{
    if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
        __tasklet_schedule(t);
}

该函数会先判断tasklet_struct结构体中的state成员,也就是tasklet当前的状态,如果tasklet还未处于TASKLET_STATE_SCHED状态的话,就会继续调用__tasklet_schedule()函数调度tasklet。

使用tasklet作为低半部处理中断的设备驱动程序简单模板代码如下所示:

/* 定义tasklet和低半部函数相关联 */
static void xxx_do_tasklet(unsigned long data)
{
    ...
    ...
    ...
}
DECLARE_TASKLET(xxx_tasklet, xxx_do_tasklet, 0);

/* 中断处理顶半部函数 */
static irqreturn_t xxx_irq_handler(int irq, void *dev_id)
{
    ...
    tasklet_schedule(&xxx_tasklet);
    ...

    return IRQ_HANDLED;
}

/* 设备驱动模块加载函数 */
static int __init xxx_init(void)
{
    int ret;

    ...
    /* 申请中断 */
    ret = request_irq(irq, xxx_irq_handler, flag, dev_name, dev_id);
    if (ret < 0)
        return ret;
    ...

    return 0;
}

/* 设备驱动模块卸载函数 */
static void __exit xxx_exit(void)
{
    ...
    /* 释放中断 */
    free(irq, dev_id);
    ...
}

module_init(xxx_init);
module_exit(xxx_exit);

在上面的程序中,xxx_init()函数是驱动模块的加载函数,在驱动模块加载的时候,使用request_irq()函数来申请需要使用的中断资源,xxx_exit()函数是模块的卸载函数,在驱动模块卸载的时候,需要使用free_irq()函数将已经申请到的中断资源释放,xxx_irq_handler()函数是中断处理顶半部,该函数的实现需要短小精悍,在该函数中使用tasklet_schedule()函数调度tasklet,xxx_do_tasklet()是中断处理低半部,由于在Linux内核的调度,该函数在恰当的时候就会得到执行。

(3.3.2)工作队列

工作队列的使用方法与tasklet非常相似,工作队列的原理是将work(需要推迟执行的函数)交由一个内核线程来执行,它总是处于进程上下文中执行,优点则是利用进程上下文来执行中断处理低半部,因此,工作队列允许重新调度和睡眠,是异步执行的进程上下文,还能解决软中断和tasklet执行时间过长导致系统实施性下降等问题。

当驱动模块程序在进程上下文中有异步执行的工作任务时,可以使用work item来描述工作任务,包括该工作任务的执行回调函数,把work item添加到一个队列中,然后一个内核线程就会去执行该工作任务的回调函数,work item被称为工作,队列被称为workqueue,也就是工作队列,内核线程被称为worker。

在Linux内核源码中,work item由struct work_struct结构体来进行描述,定义如下所示:

struct work_struct {
    atomic_long_t data;
    struct list_head entry;
    work_func_t func;
#ifdef CONFIG_LOCKDEP
    struct lockdep_map lockdep_map;
#endif
};

该结构体的成员说明如下:

  • data:用来保存work item的标志;
  • entry:用来将work item挂接到队列上;
  • func:函数指针,也就是回调函数,工作任务的处理函数。

在Linux驱动模块编程中,一般情况下,直接使用内核默认创建好的workqueue,例如:system_wq,当我们想要调度一个work时,在编写程序中,首先需要初始化一个work,然后使用schedule_work()函数,将work挂入到全局的工作队列system_wq,在Linux内核调度中,work里面的func函数就会在恰当的时候执行。

初始化一个work,可以使用INIT_WORK宏定义,该宏如下:

#define INIT_WORK(_work, _func)                        \
    __INIT_WORK((_work), (_func), 0)

#define __INIT_WORK(_work, _func, _onstack)                \
    do {                                \
        __init_work((_work), _onstack);                \
        (_work)->data = (atomic_long_t) WORK_DATA_INIT();    \
        INIT_LIST_HEAD(&(_work)->entry);            \
        (_work)->func = (_func);                \
    } while (0)

调度一个work,可以使用schedule_work()函数,该函数的定义如下:

/**
 * schedule_work - put work task in global workqueue
 * @work: job to be done
 *
 * Returns %false if @work was already on the kernel-global workqueue and
 * %true otherwise.
 *
 * This puts a job in the kernel-global workqueue if it was not already
 * queued and leaves it in the same position on the kernel-global
 * workqueue otherwise.
 */
static inline bool schedule_work(struct work_struct *work)
{
    return queue_work(system_wq, work);
}

该函数会将一个work挂入到内核的全局工作队列system_wq。

使用工作队列处理中断低半部的设备驱动程序简单模板代码如下所示:

/* 定义work和低半部处理函数 */
static void xxx_work_func(struct work_struct *work)
{
    ...
    ...
    ...
}

static struct work_struct xxx_work = {0};

/* 中断处理顶半部函数 */
static irqreturn_t xxx_irq_handler(int irq, void *dev_id)
{
    ...
    schedule_work(&xxx_work);   /* 调度work */
    ...

    return IRQ_HANDLED;
}

/* 设备驱动模块加载函数 */
static int __init xxx_init(void)
{
    int ret;

    ...
    /* 申请中断 */
    ret = request_irq(irq, xxx_irq_handler, flag, dev_name, dev_id);
    if (ret < 0)
        return ret;

    /* 初始化一个work */
    INIT_WORK(&xxx_work, &xxx_work_func);
    ...

    return 0;
}

/* 设备驱动模块卸载函数 */
static void __exit xxx_exit(void)
{
    ...
    /* 释放中断 */
    free(irq, dev_id);
    ...
}

module_init(xxx_init);
module_exit(xxx_exit);

在上述的驱动代码中,在驱动模块加载的时候,除了需要申请中断资源,还需要使用INIT_WORK宏初始化一个work,在中断的顶半部处理程序中,需要使用schedule_work()函数将用户定义的work挂入到全局工作队列system_wq中,在合适的时候,xxx_work_func()函数就会被执行。

(3.3.3)软中断

软中断softirq是用软件方式去模拟硬件中断的概念,实现宏观上的异步执行效果,tasklet的机制也是基于软中断实现的,软中断是预留给系统中对时间要求最为严格最重要的中断处理低半部使用的。

Linux内核系统静态定义了若干个软中断类型,并且Linux内核开发者不希望用户去增加新的软中断类型,软中断类型如下:

enum
{
    HI_SOFTIRQ=0,  /* 最高优先级软中断 */
    TIMER_SOFTIRQ,  /* 定时器Timer软中断 */
    NET_TX_SOFTIRQ,  /* 发送网络数据包软中断 */
    NET_RX_SOFTIRQ,  /* 接收网络数据包软中断 */
    BLOCK_SOFTIRQ,  /* 块设备软中断 */
    IRQ_POLL_SOFTIRQ,  /* IRQ轮询软中断 */
    TASKLET_SOFTIRQ,  /* tasklet机制准备的软中断 */
    SCHED_SOFTIRQ,  /* 进程调度以及负载均衡软中断 */
    HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on the
                numbering. Sigh! */
    RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */

    NR_SOFTIRQS
};

在Linux内核源码中,使用struct softirq_action结构体描述一个软中断,该结构体的定义如下所示:

struct softirq_action
{
    void    (*action)(struct softirq_action *);
};

静态定义了softirq_vec[]来表示每一个软中断对应的描述符,软中断号就是该数组的索引:

static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;

对于软中断的注册和触发,可以调用内核函数open_softirq()和raise_softirq()去实现,这两个函数的实现如下:

void open_softirq(int nr, void (*action)(struct softirq_action *))
{
    softirq_vec[nr].action = action;
}

void raise_softirq(unsigned int nr)
{
    unsigned long flags;

    local_irq_save(flags);
    raise_softirq_irqoff(nr);
    local_irq_restore(flags);
}

(3.3.4)三种低半部处理程序特点

软中断和tasklet仍然运行于中断上下文中,而工作队列则运行于进程上下文中,因此,在软中断和tasklet处理函数中不能睡眠,而工作队列处理函数中允许睡眠,如果在中断上下文中睡眠,会导致相应的硬件中断得不到及时的响应。

 

4、小结

本文主要简单介绍Linux设备驱动中断处理的一些基本知识点。

posted @ 2021-02-24 21:51  liangliangge  阅读(776)  评论(0编辑  收藏  举报