linux内核中断详解
1、中断的硬件触发流程
外设:如果外设有操作或者有数据可用,那么就会产生一个电信号,这个电信号发送给中断控制器。
中断控制器:中断控制器接收到外设发来的电信号以后,进行进一步的处理,判断这个中断是否使能或者禁止,判断它的优先级等,如果需要发送给CPU一个信号,那么中断控制器就会给cup发送一个电信号。
CPU:CPU接收到中断控制器发送过来的电信号以后,CPU就会无条件跳转到异常向量表的入口,后续CPU就处理对应的中断。
2、中断处理程序编写时的注意事项
1.中断处理函数不隶属于任何进程,所以也就不参与进程之间的调度。
2.中断处理函数要求执行的速度要快,如果不快,其他进程无法获取CPU的资源,影响系统的并发能力。
3.中断处理函数不能与用户空间进行数据的往来!如果要想让中断给用户进行数据的往来,要配合系统调用函数(file_operations)
4.在中断处理函数中,不能调用引起休眠的函数 copy_to_user, copy_from_user, 因为这些函数会进行内存的拷贝, 在拷贝的时候,有可能空闲页不够,就会引起拷贝过程进入休眠状态,等待空闲页出现!
3、共享中断
共享中断:在硬件上,多个外设接在一个中断线上,中断线接在中断控制器上。
每个设备都有自己的驱动程序,每个设备的驱动程序里面都会调用request_irq来注册中断 ,但是在注册时使用的中断号都是相同的。所以在注册中断时,一定要将中断标志或上IRQ_SHARED表示这个中断是共享的,同时dev必须是唯一的(如果中断不用时,释放中断时,由于中断号是相同的,通过dev来区分不同的中断)。如果没有使用这个宏,那么一个驱动使用这个中断号,其他的驱动就无法使用。
对于共享中断,如果中断信号来了以后,其对应的中断处理程序均会执行。因此如果使用共享中断,硬件必须支持能够判断中断是否是自己发出的(例如硬件中包含有中断状态寄存器,就可以在中断处理程序中通过中断状态寄存器的状态,判断中断是否是自己发出的)。
4、中断标志
标志 | 描述 |
---|---|
IRQF_SHARED | 多个设备共享一个中断线,共享的所有中断都必须指定此标志。如果使用共享中断的话, request_irq函数的 dev参数就是唯一区分他们的标志。 |
IRQF_ONESHOT | 单次中断,中断执行一次就 结束。即保证中断在底半部执行完之后再打开中断功能,开始接受中断。 |
IRQF_TRIGGER_NONE | 无触发。 |
IRQF_TRIGGER_RISING | 上升沿触发。 |
IRQF_TRIGGER_FALLING | 下降沿触发。 |
IRQF_TRIGGER_HIGH | 高电平触发。 |
IRQF_TRIGGER_LOW | 低电平触发。 |
IRQF_NO_SUSPEND | 在系统suspend的时候,不用disable这个中断,如果disable,可能会导致系统不能正常的resume。 |
IRQF_NO_THREAD | 有些low level的interrupt是不能线程化的(例如系统timer的中断),这个flag就是起这个作用的。 |
5、顶半部与底半部
在某些场合,中断处理函数有可能会处理相对比较耗时,比较多的事情,就会长时间的占有CPU的资源,对系统的并发能力和响应能力有很大的影响,比如网卡的数据包的读取过程。为了解决这个问题,内核将中断处理程序分为顶半部和底半部两个部分。
在顶半部里处理优先级比较高的事情,要求占用中断时间尽量的短,还要登记底半部的事情,在处理完成后,就激活底半部,由底半部处理其余任务。顶半部其实就是中断处理函数,这个过程不可中断!
底半部的处理方式主要有soft_irq, tasklet, workqueue三种,他们在使用方式和适用情况上各有不同。做相对比较耗时,不紧急的事情,这个过程可被中断。
①、如果要处理的内容不希望被其他中断打断,那么可以放到顶半部。
②、如果要处理的任务对时间敏感,可以放到顶半部。
③、如果要处理的任务与硬件有关,可以放到顶半部
④、除了上述三点以外的其他任务,优先考虑放到底半部。
6、底半部机制
6.1 软中断
soft_irq用在对底半部执行时间要求比较紧急或者非常重要的场合,主要为一些subsystem用,一般driver基本上用不上。软中断的优先级低于硬件中断,高于普通的进程。
6.2 tasklet
tasklet是基于软中断实现,它执行的上下文是软中断。它们之间的区别在于同一个tasklet同时一刻只能在一个CPU上运行,但对于软中断,同一时刻可以在多个CPU上执行,这时候在设计软中断的处理函数时,要求其函数具有可重入性(尽量避免使用全局变量,如果使用全局变量,记得要进行互斥访问的保护)。并且软中断的实现必须静态编译,不能采用模块化。相同点是它们都是工作在中断上下文中,不能做休眠的动作 。
tasklet的结构体定义在include\linux\interrupt.h中
// interrupt.h
struct tasklet_struct
{
struct tasklet_struct *next; //下一个tasklet
unsigned long state; //tasklet状态
atomic_t count; //计数器,记录tasklet的引用数
void (*func)(unsigned long); //tasklet执行的函数
unsigned long data; //函数func 的参数,可以存放普通的整形变量值,也可以存放指针,一般多存放指针
};
如果要使用 tasklet,必须先定义一个 tasklet,然后使用tasklet_init初始化tasklet。
void tasklet_init(struct tasklet_struct *t,void (*func)(unsigned long), unsigned long data);
@t:要初始化的 tasklet
@func: tasklet的处理函数。
@data 要传递给 func函数的参数
或者使用DECLARE_TASKLET一次性完成tasklet的定义和初始化。
DECLARE_TASKLET(name, func, data)
其中 name为要定义的 tasklet名字,这个名字就是一个 tasklet_struct类型的变量, func就是 tasklet的处理函数, data是传递给 func函数的参数。
在顶半部,也就是中断处理函数中调用 tasklet_schedule函数登记tasklet的工作,就能使 tasklet在合适的时间运行, tasklet_schedule函数原型如下:
void tasklet_schedule(struct tasklet_struct *t)
@t:要调度的 tasklet,也就是 DECLARE_TASKLET宏里面的 name。
tasklet的使用流程为
1、分配tasklet结构体。
struct tasklet_struct my_tasklet;
2、初始化tasklet
tasklet_init(&my_tasklet, tasklet_func, data)
@或者
DECLARE_TASKLET( my_tasklet, tasklet_func, data);
3、在中断顶半部登记tasklet
tasklet_schedule(&my_tasklet)
6.3 工作队列
tasklet和work queue在普通的driver里用的相对较多,主要区别是tasklet是在中断上下文执行不能引起休眠,而work queue是在process上下文,因此可以执行可能sleep的操作。
工作结构体定义在include\linux\workqueue.h中
struct work_struct {
atomic_long_t data; //记录工作状态和指向工作者线程的指针
struct list_head entry; //工作数据链成员
work_func_t func; //工作处理函数
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};
struct delayed_work {
struct work_struct work; //工作结构体
struct timer_list timer; //推后执行的定时器
/* target workqueue and CPU ->timer uses to queue ->work */
struct workqueue_struct *wq;
int cpu;
}; //处理延时执行的工作的结构体
工作队列结构体定义在kernel\workqueue.c中
struct workqueue_struct {
struct list_head pwqs; /* WR: all pwqs of this wq */
struct list_head list; /* PR: list of all workqueues */
struct mutex mutex; /* protects this wq */
int work_color; /* WQ: current work color */
int flush_color; /* WQ: current flush color */
atomic_t nr_pwqs_to_flush; /* flush in progress */
struct wq_flusher *first_flusher; /* WQ: first flusher */
struct list_head flusher_queue; /* WQ: flush waiters */
struct list_head flusher_overflow; /* WQ: flush overflow list */
struct list_head maydays; /* MD: pwqs requesting rescue */
struct worker *rescuer; /* MD: rescue worker */
int nr_drainers; /* WQ: drain in progress */
int saved_max_active; /* WQ: saved pwq max_active */
struct workqueue_attrs *unbound_attrs; /* PW: only for unbound wqs */
struct pool_workqueue *dfl_pwq; /* PW: only for unbound wqs */
#ifdef CONFIG_SYSFS
struct wq_device *wq_dev; /* I: for sysfs interface */
#endif
#ifdef CONFIG_LOCKDEP
char *lock_name;
struct lock_class_key key;
struct lockdep_map lockdep_map;
#endif
char name[WQ_NAME_LEN]; /* I: workqueue name */
/*
* Destruction of workqueue_struct is RCU protected to allow walking
* the workqueues list without grabbing wq_pool_mutex.
* This is used to dump all workqueues from sysrq.
*/
struct rcu_head rcu;
/* hot fields used during command issue, aligned to cacheline */
unsigned int flags ____cacheline_aligned; /* WQ: WQ_* flags */
struct pool_workqueue __percpu *cpu_pwqs; /* I: per-cpu pwqs */
struct pool_workqueue __rcu *numa_pwq_tbl[]; /* PWR: unbound pwqs indexed by node */
};
Linux内核使用工作者线程 (worker thread)来处理工作队列中的各个工作, Linux内核使用worker结构体表示工作者线程。
struct worker {
/* on idle list while idle, on busy hash table while busy */
union {
struct list_head entry; /* L: while idle */
struct hlist_node hentry; /* L: while busy */
};
struct work_struct *current_work; /* L: work being processed */
work_func_t current_func; /* L: current_work's fn */
struct pool_workqueue *current_pwq; /* L: current_work's pwq */
struct list_head scheduled; /* L: scheduled works */
/* 64 bytes boundary on 64bit, 32 on 32bit */
struct task_struct *task; /* I: worker task */
struct worker_pool *pool; /* A: the associated pool */
/* L: for rescuers */
struct list_head node; /* A: anchored at pool->workers */
/* A: runs through worker->node */
unsigned long last_active; /* L: last active timestamp */
unsigned int flags; /* X: flags */
int id; /* I: worker id */
int sleeping; /* None */
/*
* Opaque string set with work_set_desc(). Printed out with task
* dump for debugging - WARN, BUG, panic or sysrq.
*/
char desc[WORKER_DESC_LEN];
/* used only by rescuers to point to the target workqueue */
struct workqueue_struct *rescue_wq; /* I: the workqueue to rescue */
/* used by the scheduler to determine a worker's last known identity */
work_func_t last_func;
};
可以看出,每个 worker都有一个工作队列,工作者线程处理自己工作队列中的所有工作。在实际的驱动开发中,我们只需要定义工作 (work_struct)即可,关于工作队列和工作者线程我们基本不用去管。
如果要使用工作队列 ,首先需要定义一个work_struct结构体变量,然后使用 INIT_WORK宏来初始化工作。
#define INIT_WORK(_work, _func) \
__INIT_WORK((_work), (_func), 0)
#define INIT_DELAYED_WORK(_work, _func) \
__INIT_DELAYED_WORK(_work, _func, 0)
@_work:要初始化的工作
@_func: 工作对应的处理函数
也可以使用DECLARE_WORK宏一次性完成工作的创建和初始化。
#define DECLARE_WORK(n, f) \
struct work_struct n = __WORK_INITIALIZER(n, f)
#define DECLARE_DELAYED_WORK(n, f) \
struct delayed_work n = __DELAYED_WORK_INITIALIZER(n, f, 0)
@n:定义的工作work_struct
@f:工作对应的处理函数
和 tasklet一样,工作也是需要登记才能调度运行,工作的登记函数为 schedule_work
static inline bool schedule_work(struct work_struct *work)
{
return queue_work(system_wq, work);
}
static inline bool schedule_delayed_work(struct delayed_work *dwork,
unsigned long delay)
{
return queue_delayed_work(system_wq, dwork, delay);
}
@work:要调度的工作
@delay:延时调度的延时时间
工作的使用流程为
1、分配work_struct结构体
struct work_struct my_work;
struct delayed_work_struct my_dwork;
2、初始化工作
INIT_WORK(&my_work, my_work_func);
INIT_DELAYED_WORK(&my_dwork, my_dwork_func);
@或者
DECLARE_WORK(my_work, my_work_func);
DECLARE_DELAYED_WORK(my_dwork, my_dwork_func)
3、在顶半部中断中登记工作
schedule_work(&my_work)
schedule_delayed_work(&my_dwork, times)
在Linux kernel中,一个外设的中断处理被分成top half和bottom half,top half进行最关键,最基本的处理,而比较耗时的操作被放到bottom half(softirq、tasklet)中延迟执行。虽然bottom half被延迟执行,但始终都是先于进程执行的。为何不让这些耗时的bottom half和普通进程公平竞争呢?因此,linux kernel借鉴了RTOS的某些特性,对那些耗时的驱动interrupt handler进行线程化处理,在内核的抢占点上,让线程(无论是内核线程还是用户空间创建的线程,还是驱动的interrupt thread)在一个舞台上竞争CPU。
6.4 线程化irq
7、API函数
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev)
在 Linux内核中要想使用某个中断是需要申请的, request_irq函数用于申请中断, request_irq函数可能会导致睡眠,因此不能在中断上下文或者其他禁止睡眠的代码段中使用 request_irq函数。 request_irq函数会激活 (使能 )中断,所以不需要我们手动去使能中断。
@irq:要申请中断的中断号。
@handler:中断处理函数,当中断发生以后就会执行此中断处理函数。
@flags:中断标志,可以在文件 include/linux/interrupt.h里面查看所有的中断标志。
@name:中断名字,设置以后可以在 /proc/interrupts文件中看到对应的中断名字。
@dev 如果将 flags设置为 IRQF_SHARED的话, dev用来区分不同的中断,一般情况下将dev设置为设备结构体, dev会传递给中断处理函数 irq_handler_t的第二个参数。
@返回值: 0 中断申请成功,其他负值 中断申请失败,如果返回 -EBUSY的话表示中断已经被申请了。
void free_irq(unsigned int irq, void *dev)
使用中断的时候需要通过 request_irq函数申请,使用完成以后就要通过 free_irq函数释放掉相应的中断。如果中断不是共享的,那么 free_irq会删除中断处理函数并且禁止中断。
@irq: 要释放的中断。
@dev:如果中断设置为共享 (IRQF_SHARED)的话,此参数用来区分具体的中断。共享中断只有在释放最后中断处理函数的时候才会被禁止掉。
irqreturn_t (*irq_handler_t) (int, void *)
使用 request_irq函数申请中断的时候需要设置中断处理函数。
第一个参数是要中断处理函数要相应的中断号。第二个参数是一个指向 void的指针,也就是个通用指针,需要与 request_irq函数的 dev参数保持一致。用于区分共享中断的不同设备,dev也可以指向设备数据结构。
int request_threaded_irq(unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsigned long irqflags, const char *devname, void *dev_id)
输入参数 | 描述 |
---|---|
irq | 要注册handler的那个IRQ number。这里要注册的handler包括两个,一个是传统意义的中断handler,我们称之primary handler(类似于顶半部),另外一个是threaded interrupt handler(类似于底半部) |
handler | primary handler。需要注意的是primary handler和threaded interrupt handler不能同时为空,否则会出错。如果该函数结束时,返回的是IRQ_WAKE_THREAD,内核会调度对应线程执行thread_fn对应的函数。该参数可以设置为空,这种情况下内核会默认的irq_default_primary_handler()代替handler,并会使用IRQF_ONESHOT标记。 |
thread_fn | threaded interrupt handler。如果该参数不是NULL,那么系统会创建一个kernel thread,调用的function就是thread_fn |
irqflags | 参见本章第4节中断标志,这里支持设置IRQF_ONESHOT标记,这样内核会自动帮助我们在中断上下文屏蔽对应的中断号,而在内核调度thread_fn执行后,重新使能该中断号。对于我们无法在顶半部清楚中断的情况,IRQF_ONESHOT特别有用。 |
devname | 请求中断的设备名称 |
dev_id | 传递给中断处理函数handler的参数,如果是使用IRQF_SHARED时,用于区分不同的中断。 |
int devm_request_threaded_irq(struct device *dev, unsigned int irq,
irq_handler_t handler, irq_handler_t thread_fn,
unsigned long irqflags, const char *devname,
void *dev_id)
{
struct irq_devres *dr;
int rc;
dr = devres_alloc(devm_irq_release, sizeof(struct irq_devres),
GFP_KERNEL);
if (!dr)
return -ENOMEM;
if (!devname)
devname = dev_name(dev);
rc = request_threaded_irq(irq, handler, thread_fn, irqflags, devname,
dev_id);
if (rc) {
devres_free(dr);
return rc;
}
dr->irq = irq;
dr->dev_id = dev_id;
devres_add(dev, dr);
return 0;
}
从函数实现可以看出,该函数的实现是通过request_threaded_irq实现的。该函数与request_threaded_irq的区别在于,该函数在驱动程序分离时,能够自动释放中断线。
如果释放该函数分配的中断线需要使用devm_free_irq
void devm_free_irq(struct device *dev, unsigned int irq, void *dev_id)
/**
* disable_irq - disable an irq and wait for completion
* @irq: Interrupt to disable
*
* Disable the selected interrupt line. Enables and Disables are
* nested.
* This function waits for any pending IRQ handlers for this interrupt
* to complete before returning. If you use this function while
* holding a resource the IRQ handler may need you will deadlock.
*
* This function may be called - with care - from IRQ context.
*/
void disable_irq(unsigned int irq)
{
if (!__disable_irq_nosync(irq))
synchronize_irq(irq);
}
EXPORT_SYMBOL(disable_irq);
如果在n号中断的顶半部调用disable_irq(n),会引起系统的死锁,在这种情况下只能调用disable_irq_nosync(n)。
/**
* disable_irq_nosync - disable an irq without waiting
* @irq: Interrupt to disable
*
* Disable the selected interrupt line. Disables and Enables are
* nested.
* Unlike disable_irq(), this function does not ensure existing
* instances of the IRQ handler have completed before returning.
*
* This function may be called from IRQ context.
*/
void disable_irq_nosync(unsigned int irq)
{
__disable_irq_nosync(irq);
}
/**
* enable_irq - enable handling of an irq
* @irq: Interrupt to enable
*
* Undoes the effect of one call to disable_irq(). If this
* matches the last disable, processing of interrupts on this
* IRQ line is re-enabled.
*
* This function may be called from IRQ context only when
* desc->irq_data.chip->bus_lock and desc->chip->bus_sync_unlock are NULL !
*/
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);
}
8 request_threaded_irq分析
int request_threaded_irq(unsigned int irq, irq_handler_t handler,
irq_handler_t thread_fn, unsigned long irqflags,
const char *devname, void *dev_id)
{
struct irqaction *action;
struct irq_desc *desc;
int retval;
if (irq == IRQ_NOTCONNECTED)
return -ENOTCONN;
/*
* Sanity-check: shared interrupts must pass in a real dev-ID,
* otherwise we'll have trouble later trying to figure out
* which interrupt is which (messes up the interrupt freeing
* logic etc).
*
* Also IRQF_COND_SUSPEND only makes sense for shared interrupts and
* it cannot be set along with IRQF_NO_SUSPEND.
*/
if (((irqflags & IRQF_SHARED) && !dev_id) ||
(!(irqflags & IRQF_SHARED) && (irqflags & IRQF_COND_SUSPEND)) ||
((irqflags & IRQF_NO_SUSPEND) && (irqflags & IRQF_COND_SUSPEND)))
return -EINVAL;
desc = irq_to_desc(irq);
if (!desc)
return -EINVAL;
if (!irq_settings_can_request(desc) ||
WARN_ON(irq_settings_is_per_cpu_devid(desc)))
return -EINVAL;
if (!handler) {
if (!thread_fn)
return -EINVAL;
handler = irq_default_primary_handler;
}
action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
if (!action)
return -ENOMEM;
action->handler = handler;
action->thread_fn = thread_fn;
action->flags = irqflags;
action->name = devname;
action->dev_id = dev_id;
retval = irq_chip_pm_get(&desc->irq_data);
if (retval < 0) {
kfree(action);
return retval;
}
retval = __setup_irq(irq, desc, action);
if (retval) {
irq_chip_pm_put(&desc->irq_data);
kfree(action->secondary);
kfree(action);
}
#ifdef CONFIG_DEBUG_SHIRQ_FIXME
if (!retval && (irqflags & IRQF_SHARED)) {
/*
* It's a shared IRQ -- the driver ought to be prepared for it
* to happen immediately, so let's make sure....
* We disable the irq to make sure that a 'real' IRQ doesn't
* run in parallel with our fake.
*/
unsigned long flags;
disable_irq(irq);
local_irq_save(flags);
handler(irq, dev_id);
local_irq_restore(flags);
enable_irq(irq);
}
#endif
return retval;
}
EXPORT_SYMBOL(request_threaded_irq);
首先是判断传入参数的安全性
if (((irqflags & IRQF_SHARED) && !dev_id) ||
(!(irqflags & IRQF_SHARED) && (irqflags & IRQF_COND_SUSPEND)) ||
((irqflags & IRQF_NO_SUSPEND) && (irqflags & IRQF_COND_SUSPEND)))
return -EINVAL;
如果设置了IRQF_SHARED但是没有传入dev_id时,会直接返回错误,因为这样的设置在释放中断函数的时候回出错。
desc = irq_to_desc(irq);
通过中断号获取中断描述符。在linux kernel中,对于每一个外设的IRQ都用struct irq_desc来描述,我们称之中断描述符(struct irq_desc)。linux kernel中会有一个数据结构保存了关于所有IRQ的中断描述符信息,我们称之中断描述符DB。当发生中断后,首先获取触发中断的HW interupt ID,然后通过irq domain翻译成IRQ number,然后通过IRQ number就可以获取对应的中断描述符。调用中断描述符中的highlevel irq-events handler来进行中断处理就OK了。
if (!irq_settings_can_request(desc) ||
WARN_ON(irq_settings_is_per_cpu_devid(desc)))
return -EINVAL;
并非系统中所有的IRQ number都可以request,有些中断描述符被标记为IRQ_NOREQUEST,标识该IRQ number不能被其他的驱动request。一般而言,这些IRQ number有特殊的作用,例如用于级联的那个IRQ number是不能request。irq_settings_can_request函数就是判断一个IRQ是否可以被request。
if (!handler) {
if (!thread_fn)
return -EINVAL;
handler = irq_default_primary_handler;
这里是对传入的两个函数进行判断
primary handler | threaded handler | 描述 |
---|---|---|
NULL | NULL | 函数出错,返回-EINVAL |
设定 | 设定 | 正常流程。中断处理被合理的分配到primary handler和threaded handler中。 |
设定 | NULL | 中断处理都是在primary handler中完成 |
NULL | 设定 | 这种情况下,系统会帮忙设定一个default的primary handler:irq_default_primary_handler,协助唤醒threaded handler线程 |
action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
if (!action)
return -ENOMEM;
action->handler = handler;
action->thread_fn = thread_fn;
action->flags = irqflags;
action->name = devname;
action->dev_id = dev_id;
retval = irq_chip_pm_get(&desc->irq_data);
if (retval < 0) {
kfree(action);
return retval;
}
retval = __setup_irq(irq, desc, action);
这部分的代码很简单,分配struct irqaction,赋值,调用__setup_irq进行实际的注册过程。