一、中断的概念
中断是指在CPU正常运行期间,由于内外部事件或由程序预先安排的事件引起的CPU暂时停止正在运行的程序,转而为该内部或外部事件或预先安排的事件服务的程序中去,服务完毕后再返回去继续运行被暂时中断的程序。Linux中通常分为外部中断(又叫硬件中断)和内部中断(又叫异常)。
在实地址模式中,CPU把内存中从0开始的1KB空间作为一个中断向量表。表中的每一项占4个字节。但是在保护模式中,有这4个字节的表项构成的中断向量表不满足实际需求,于是根据反映模式切换的信息和偏移量的足够使得中断向量表的表项由8个字节组成,而中断向量表也叫做了中断描述符表(IDT)。在CPU中增加了一个用来描述中断描述符表寄存器(IDTR),用来保存中断描述符表的起始地址。
二、中断的请求过程
外部设备当需要操作系统做相关的事情的时候,会产生相应的中断。设备通过相应的中断线向中断控制器发送高电平以产生中断信号,而操作系统则会从中断控制器的状态位取得那根中断线上产生的中断。而且只有在设备在对某一条中断线拥有控制权,才可以向这条中断线上发送信号。也由于现在的外设越来越多,中断线又是很宝贵的资源不可能被一一对应。因此在使用中断线前,就得对相应的中断线进行申请。无论采用共享中断方式还是独占一个中断,申请过程都是先讲所有的中断线进行扫描,得出哪些没有别占用,从其中选择一个作为该设备的IRQ。其次,通过中断申请函数申请相应的IRQ。最后,根据申请结果查看中断是否能够被执行。
中断机制的核心数据结构是 irq_desc, 它完整地描述了一条中断线 (或称为 “中断通道” )。以下程序源码版本为linux-2.6.32.2。
其中irq_desc 结构在 include/linux/irq.h 中定义:
typedef void (*irq_flow_handler_t)(unsigned int irq, struct irq_desc *desc);
struct irq_desc {
unsigned int irq;
struct timer_rand_state *timer_rand_state;
unsigned int *kstat_irqs;
#ifdef CONFIG_INTR_REMAP
struct irq_2_iommu *irq_2_iommu;
#endif
irq_flow_handler_t handle_irq;
struct irq_chip *chip;
struct msi_desc *msi_desc;
void *handler_data;
void *chip_data;
struct irqaction *action;
unsigned int status;
unsigned int depth;
unsigned int wake_depth;
unsigned int irq_count;
unsigned long last_unhandled;
unsigned int irqs_unhandled;
spinlock_t lock;
#ifdef CONFIG_SMP
cpumask_var_t affinity;
unsigned int node;
#ifdef CONFIG_GENERIC_PENDING_IRQ
cpumask_var_t pending_mask;
#endif
#endif
atomic_t threads_active;
wait_queue_head_t wait_for_threads;
#ifdef CONFIG_PROC_FS
struct proc_dir_entry *dir;
#endif
const char *name;
} ____cacheline_internodealigned_in_smp;
I、Linux中断的申请与释放:在<linux/interrupt.h>, , 实现中断申请接口:
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,const char *name, void *dev);
函数参数说明
unsigned int irq:所要申请的硬件中断号
irq_handler_t handler:中断服务程序的入口地址,中断发生时,系统调用handler这个函数。irq_handler_t为自定义类型,其原型为:
typedef irqreturn_t (*irq_handler_t)(int, void *);
而irqreturn_t的原型为:typedef enum irqreturn irqreturn_t;
enum irqreturn {
IRQ_NONE,
IRQ_HANDLED,
IRQ_WAKE_THREAD,
};
在枚举类型irqreturn定义在include/linux/irqreturn.h文件中。
unsigned long flags:中断处理的属性,与中断管理有关的位掩码选项,有一下几组值:
#define IRQF_DISABLED 0x00000020
#define IRQF_SAMPLE_RANDOM 0x00000040
#define IRQF_SHARED 0x00000080
#define IRQF_PROBE_SHARED 0x00000100
#define IRQF_TIMER 0x00000200
#define IRQF_PERCPU 0x00000400
#define IRQF_NOBALANCING 0x00000800
#define IRQF_IRQPOLL 0x00001000
#define IRQF_ONESHOT 0x00002000
#define IRQF_TRIGGER_NONE 0x00000000
#define IRQF_TRIGGER_RISING 0x00000001
#define IRQF_TRIGGER_FALLING 0x00000002
#define IRQF_TRIGGER_HIGH 0x00000004
#define IRQF_TRIGGER_LOW 0x00000008
#define IRQF_TRIGGER_MASK (IRQF_TRIGGER_HIGH | IRQF_TRIGGER_LOW | \
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING)
#define IRQF_TRIGGER_PROBE 0x00000010
const char *dev_name:设备描述,表示那一个设备在使用这个中断。
void *dev_id:用作共享中断线的指针.。一般设置为这个设备的设备结构体或者NULL。由于现在的外设越来越多,中断线又是很宝贵的资源不可能被一一对应,所以中断线会被共享。dev_id是唯一的,用来标识唯一的外设设备,以及可能还被驱动用来指向它自己的私有数据区,来标识哪个设备在中断 。这个参数在真正的驱动程序中一般是指向设备数据结构的指针.在调用中断处理程序的时候它就会传递给中断处理程序的void *dev_id。如果中断没有被共享, dev_id 可以设置为 NULL。
II、释放IRQ
void free_irq(unsigned int irq, void *dev_id);
III、中断线共享的数据结构
struct irqaction {
irq_handler_t handler;
unsigned long flags;
const char *name;
void *dev_id;
struct irqaction *next;
int irq;
struct proc_dir_entry *dir;
irq_handler_t thread_fn;
struct task_struct *thread;
unsigned long thread_flags;
};
thread_flags参见枚举型
enum {
IRQTF_RUNTHREAD,
IRQTF_DIED,
IRQTF_WARNED,
IRQTF_AFFINITY,
};
多个中断处理程序可以共享同一条中断线,irqaction 结构中的 next 成员用来把共享同一条中断线的所有中断处理程序组成一个单向链表,dev_id 成员用于区分各个中断处理程序。
根据以上内容可以得出中断机制各个数据结构之间的联系如下图所示:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyN5MzNwQjM4ETOwkDM1EDMy8CX0Vmbu4GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
三.中断的处理过程
Linux中断分为两个半部:顶半部(tophalf)和底半部(bottom half)。上半部的功能是"登记中断",当一个中断发生时,它进行相应地硬件读写后就把中断例程的下半部挂到该设备的下半部执行队列中去。因此,上半部执行的速度就会很快,可以服务更多的中断请求。但是,仅有"登记中断"是远远不够的,因为中断的事件可能很复杂。因此,Linux引入了一个下半部,来完成中断事件的绝大多数使命。下半部和上半部最大的不同是下半部是可中断的,而上半部是不可中断的,下半部几乎做了中断处理程序所有的事情,而且可以被新的中断打断!下半部则相对来说并不是非常紧急的,通常还是比较耗时的,因此由系统自行安排运行时机,不在中断服务上下文中执行。
中断号的查看可以使用下面的命令:“cat /proc/interrupts”。
Linux实现下半部的机制主要有tasklet和工作队列。
小任务tasklet的实现
其数据结构为struct tasklet_struct,每一个结构体代表一个独立的小任务,定义如下
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
state的取值参照下边的枚举型:
enum
{
TASKLET_STATE_SCHED,
TASKLET_STATE_RUN
};
count域是小任务的引用计数器。只有当它的值为0的时候才能被激活,并其被设置为挂起状态时,才能够被执行,否则为禁止状态。
I、声明和使用小任务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 }
这两个宏的区别在于计数器设置的初始值不同,前者可以看出为0,后者为1。为0的表示激活状态,为1的表示禁止状态。其中ATOMIC_INIT宏为:
#define ATOMIC_INIT(i) { (i) }
即可看出就是设置的数字。此宏在include/asm-generic/atomic.h中定义。这样就创建了一个名为name的小任务,其处理函数为func。当该函数被调用的时候,data参数就被传递给它。
II、小任务处理函数程序
处理函数的的形式为:void my_tasklet_func(unsigned long data)。这样DECLARE_TASKLET(my_tasklet, my_tasklet_func, data)实现了小任务名和处理函数的绑定,而data就是函数参数。
III、调度编写的tasklet
调度小任务时引用tasklet_schedule(&my_tasklet)函数就能使系统在合适的时候进行调度。函数原型为:
static inline void tasklet_schedule(struct tasklet_struct *t)
{
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
__tasklet_schedule(t);
}
这个调度函数放在中断处理的上半部处理函数中,这样中断申请的时候调用处理函数(即irq_handler_t handler)后,转去执行下半部的小任务。
如果希望使用DECLARE_TASKLET_DISABLED(name,function,data)创建小任务,那么在激活的时候也得调用相应的函数被使能
tasklet_enable(struct tasklet_struct *); //使能tasklet
tasklet_disble(struct tasklet_struct *); //禁用tasklet
tasklet_init(struct tasklet_struct *,void (*func)(unsigned long),unsigned long);
当然也可以调用tasklet_kill(struct tasklet_struct *)从挂起队列中删除一个小任务。清除指定tasklet的可调度位,即不允许调度该tasklet 。
使用tasklet作为下半部的处理中断的设备驱动程序模板如下:
void my_do_tasklet(unsigned long);
DECLARE_TASKLET(my_tasklet, my_do_tasklet, 0);
void my_do_tasklet(unsigned long)
{
……
}
irpreturn_t my_interrupt(unsigned int irq,void *dev_id)
{
……
tasklet_schedule(&my_tasklet)
……
}
int __init xxx_init(void)
{
……
result=request_irq(my_irq,my_interrupt,IRQF_DISABLED,"xxx",NULL);
……
}
void __exit xxx_exit(void)
{
……
free_irq(my_irq,my_interrupt);
……
}
工作队列的实现
工作队列work_struct结构体,位于/include/linux/workqueue.h
typedef void (*work_func_t)(struct work_struct *work);
struct work_struct {
atomic_long_t data;
#define WORK_STRUCT_PENDING 0
#define WORK_STRUCT_FLAG_MASK (3UL)
#define WORK_STRUCT_WQ_DATA_MASK (~WORK_STRUCT_FLAG_MASK)
struct list_head entry;
work_func_t func;
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};
这些结构被连接成链表。当一个工作者线程被唤醒时,它会执行它的链表上的所有工作。工作被执行完毕,它就将相应的work_struct对象从链表上移去。当链表上不再有对象的时候,它就会继续休眠。可以通过DECLARE_WORK在编译时静态地创建该结构,以完成推后的工作。
#define DECLARE_WORK(n, f) \
struct work_struct n = __WORK_INITIALIZER(n, f)
而后边这个宏为一下内容:
#define __WORK_INITIALIZER(n, f) { \
.data = WORK_DATA_INIT(), \
.entry = { &(n).entry, &(n).entry }, \
.func = (f), \
__WORK_INIT_LOCKDEP_MAP(#n, &(n)) \
}
其为参数data赋值的宏定义为:
#define WORK_DATA_INIT() ATOMIC_LONG_INIT(0)
这样就会静态地创建一个名为n,待执行函数为f,参数为data的work_struct结构。同样,也可以在运行时通过指针创建一个工作:
INIT_WORK(struct work_struct *work, void(*func) (void *));
这会动态地初始化一个由work指向的工作队列,并将其与处理函数绑定。宏原型为:
#define INIT_WORK(_work, _func) \
do { \
static struct lock_class_key __key; \
\
(_work)->data = (atomic_long_t) WORK_DATA_INIT(); \
lockdep_init_map(&(_work)->lockdep_map, #_work, &__key, 0);\
INIT_LIST_HEAD(&(_work)->entry); \
PREPARE_WORK((_work), (_func)); \
} while (0)
在需要调度的时候引用类似tasklet_schedule()函数的相应调度工作队列执行的函数schedule_work(),如:
schedule_work(&work);
如果有时候并不希望工作马上就被执行,而是希望它经过一段延迟以后再执行。在这种情况下,可以调度指定的时间后执行函数:
schedule_delayed_work(&work,delay);函数原型为:
int schedule_delayed_work(struct delayed_work *work, unsigned long delay);
其中是以delayed_work为结构体的指针,而这个结构体的定义是在work_struct结构体的基础上增加了一项timer_list结构体。
struct delayed_work {
struct work_struct work;
struct timer_list timer;
};
这样,便使预设的工作队列直到delay指定的时钟节拍用完以后才会执行。
使用工作队列处理中断下半部的设备驱动程序模板如下:
struct work_struct my_wq;
void my_do_work(unsigned long);
void my_do_work(unsigned long)
{
……
}
irpreturn_t my_interrupt(unsigned int irq,void *dev_id)
{
……
schedule_work(&my_wq)
……
}
int __init xxx_init(void)
{
……
result=request_irq(my_irq,my_interrupt,IRQF_DISABLED,"xxx",NULL);
……
INIT_WORK(&my_irq,(void (*)(void *))my_do_work);
……
}
void __exit xxx_exit(void)
{
……
free_irq(my_irq,my_interrupt);
……
}
记住在中断上下文中不能使用信号量 down()等函数,down()获取不到信号量会进入休眠状态,影响程序结果!!!
Linux内核定时器与使用方法
一.度量时间差
时钟中断是由系统的定时硬件以周期性的时间间隔产生,这个间隔(即频率)由内核根据HZ来确定,HZ是一个与体系结构无关的常量(定义在),可配置(50-1200),在X86平台,默认值为1000.HZ的含义是系统每秒钟产生的时钟中断的次数.可以说 HZ=1s。
每当时钟中断发生时,全局变量jiffies(一个32位的unsigned long变量,定义在)就加1,因此jiffies记录了字linux系统启动后时钟中断发生的次数.驱动程序常利用jiffies来计算不同事件间的时间间隔.
2.6内核的时钟中断频率是1000,也就是说,在1秒里jiffies会被增加1000。因此jiffies + 2 * HZ表示推后2秒钟。
在定时器应用中经常需要比较两个时间值,以确定timer是否超时,所以Linux内核在timer.h头文件中定义了4个时间关系比较操作宏。这里我们说时刻a在时刻b之后,就意味着时间值a≥b。Linux强烈推荐用户使用它所定义的下列4个时间比较操作宏(include/linux/timer.h):
#include
int time_after(unsigned long a, unsigned long b);
int time_before(unsigned long a, unsigned long b);
int time_after_eq(unsigned long a, unsigned long b);
int time_after_eq(unsigned long a, unsigned long b);
获取当前时间:
#include
struct timeval {
time_t tv_sec;
suseconds_t tv_usec;
};
void do_gettimeofday(struct timeval *tv)
二.内核定时器
内核定时器用于控制某个函数(定时器处理函数)在未来的某个特定时间执行.内核定时器注册的处理函数只执行一次.处理过后即失效.
当内核定时器被调度运行时,几乎可以肯定其不会在注册这些函数的进程正在执行时.相反,它会异步的执行.这种异步类似于硬件中断发生时的情景.实际上,内核定时器是被"软件中断"调度运行的.因此,其运行于原子上下文中.这点和tasklet很类似.处于原子上下文的进程有一些运行时的限制:
1. 不能访问用户空间.因为没有进程上下文.无法与特定的进程与用户关联
2. 不能执行调度或休眠.
3. Current指针在原子模式下无意义.
内核定时器被组织成双向链表,使用struct timer_list结构描述.
struct time_list{
unsigned long expires; //超时的jiffies值
void(*function)(unsigned long) ; //注册的定时器处理函数
unsigned long data; //定时器处理函数的参数
}
这3个字段表示,当jiffies等于expires时,内核会调度function函数运行.data是传递给function的参数的指针,如果function函数需要不止一个参数,那么可以将这几个参数组成一个结构体,并将结构体的指针赋值给data.
三.管理定时器的接口
void init_timer(struct time_list *timer);
初始化定时器队列结构.timer_list结构在使用前必须初始化,这是要保证结构体中其他的成员能正确赋值.
void add_timer(struct time_list *timer);
启动定时器.
int del_timer(struct time_list *timer);
在定时器超时前将定时器删除.当定时器超时后,系统会自动将其删除.
四.内核定时器的使用方法
#include // for timer_list API
#include // for HZ
#include // for jiffies
struct timer_list my_timer;
init_timer(&my_timer);
my_timer.expires = jiffies + delay_sec*HZ; //延时delay_sec秒
my_timer.data = **; //给定时器处理函数传入的参数
my_timer.function = my_function; //定时器超时时调用的函数
add_timer(&my_timer);
mod_timer(&my_timer);
result = del_timer(&my_timer);
当删除定时器时,必须小心一个潜在的竞争条件。当del_timer()返回后,可以保证的只是:定时器不会再被激活(也就是,将来不会执行),但是在多处理机器上定时器中断可能已经在其他处理器上运行了,所以删除定时器时需要等待可能在其他处理器上运行的定时器处理程序都退出,这时就要使用del_timer_sync()函数执行删除工作:
del_timer_sync(&my_timer);
和del_timer()函数不同,del_timer_sync()函数不能在中断上下文中使用.
五.短延时
当一个设备驱动需要处理它的硬件的反应时间, 涉及到的延时常常是最多几个毫秒.
内核函数 ndelay, udelay, 以及 mdelay 对于短延时好用, 分别延后执行指定的纳秒数, 微秒数或者毫秒数. 它们的原型是:
#include void ndelay(unsigned long nsecs); void udelay(unsigned long usecs); void mdelay(unsigned long msecs);
有另一个方法获得毫秒(和更长)延时而不用涉及到忙等待. 文件 声明这些函数:
void msleep(unsigned int millisecs); unsigned long msleep_interruptible(unsigned int millisecs); void ssleep(unsigned int seconds)
前 2 个函数使调用进程进入睡眠给定的毫秒数. 一个对 msleep 的调用是不可中断的; 你能确保进程睡眠至少给定的毫秒数. 如果你的驱动位于一个等待队列并且你想唤醒来打断睡眠, 使用 msleep_interruptible. 从 msleep_interruptible 的返回值正常地是 0; 如果, 但是, 这个进程被提早唤醒, 返回值是在初始请求睡眠周期中剩余的毫秒数. 对 ssleep 的调用使进程进入一个不可中断的睡眠给定的秒数.