10.1 中断与定时器
- 中断的划分
- 来源:中断分为内部中断(CPU内部)和外部中断(外设提出的请求)
- 是否可屏蔽:可屏蔽中断与不可屏蔽中断(NMI)
- 入口跳转方法:中断可分为向量中断和非向量中断(向量中断由硬件提供中断服务程序入口地址,非向量中断由软件提供中断服务程序入口地址)
- 向量中断:CPU通常为不同的中断分配不同的中断号,当检测到某中断号的中断到来后,就自动跳转到与该中断号对应的地址执行。
- 非向量中断:非向量中断的多个中断共享一个入口地址,进入该入口地址后,再通过软件判断中断标志来识别具体是哪个中断。
- ARM中中断控制器为GIC,支持三种类型中断:
- SGI(Software Generated Interrupt):软件产生的中断,可以用于多核的核间通信,一个CPU可以通过写GIC的寄存器给另外一个CPU产生中断。
- PPI(Private Peripheral Interrupt):某个CPU私有外设的中断,这类外设的中断只能发给绑定的那个CPU。
- SPI(Shared Peripheral Interrupt):共享外设的中断,这类外设的中断可以路由到任何一个CPU。
- 对于SPI类型的中断,内核可以通过如下API设定中断触发的CPU核:
extern int irq_set_affinity (unsigned int irq, const struct cpumask *m);
10.2 Linux中断处理程序架构
- Linux将中断处理程序分解为两个半部:顶半部(Top Half)和底半部(Bottom Half)
- 顶半部完成尽量少的比较紧急的功能(一般读取寄存器中的中断状态,并在清除中断标志后就进行“登记中断”的工作)
- 底半部来完成中断事件的绝大多数任务。而且可以被新的中断打断。
- 通过查看 /proc/interrupts文件可以获得系统中断的统计信息,可以统计每一个中断号上的中断在每个CPU上发生的次数
10.3 Linux中断编程
10.3.1 申请和释放中断
- 申请和释放中断:
-
申请irq
申请中断
1
int
request_irq(unsigned
int
irq, irq_handler_t handler, unsigned
long
flags,
const
char
*name,
void
*dev)
irq: 要申请的硬件中断号
handler:中断处理函数(顶半部)
irqflags:中断标志,在文件 include/linux/interrupt.h 里面,常用的中断处理标志:
name:中断名字
dev: 如果将 flags 设置为 IRQF_SHARED 的话,dev 用来区分不同的中断,一般情况下将 dev 设置为设备结构体,dev 会传递给中断处理函数 irq_handler_t 的第二个参数。
返回值:返回0成功,-EINVAL表示中断号无效或处理函数指针为NULL,-EBUSY表示中断已经被占用且不能共享 -
中断处理函数(顶半部):
顶半部中断处理函数
1
irqreturn_t (*irq_handler_t)(
int
,
void
*)
第一个参数为中断号,第二个参数和 request_irq() 函数中的dev参数相同
-
释放irq:
1
void
free_irq(unsigned
int
irq,
void
*dev);
参数同request_irq()函数。
-
10.3.2 使能和屏蔽中断
- 中断使能和禁止(单个中断):
-
中断使能:
void
enable_irq(unsigned
int
irq)
- 中断禁止:
-
void
disable_irq(unsigned
int
irq)
等中断处理完成才返回
-
void
disable_irq_nosync(unsigned
int
irq)
立即返回
-
- 不能在中断上半部调用 disabled_irq() 函数,由于会等待中断处理完毕,因此会形成死锁。此时只能调用 disable_irq_nosync()
- 全局中断使能和关闭(只对本CPU有效):
- local_irq_enable()
- void local_irq_disable(void)
- 全局中断保存和恢复(仅对本CPU有效):
- 禁用并中断保存:local_irq_save(flags)
- 中断恢复:local_irq_restore(flags)
-
10.3.3 底半部机制
- Linux实现底半部的机制主要有tasklet、工作队列、软中断和线程化irq。
-
tasklet
- 执行上下文是软中断,执行时机一般是顶半部返回的时候
- 首先定义一个tasklet的处理函数,然后通过宏DECLEAR_TASKLET(my_tasklet, my_tasklet_func, data)将tasklet和其处理函数绑定,最后在需要调度时使用tasklet_schedule(&my_tasklet)函数。
- tasklet使用模板:
-
工作队列
- 执行上下文是内核线程,因此可以调度和睡眠
- 定义工作队列和处理函数:
- 初始化工作队列:
- 工作队列使用方法:
-
软中断
- 执行时机是顶半部返回的时候,tasklet是基于软中断实现的
- 使用结构体softirq_action表示软中断(成员变量为中断处理函数指针)
- 使用open_softirq()函数进行注册软中断处理函数,使用raise_softirq()触发软中断
- 禁止和使能软中断和tasklet:local_bh_disable()和local_bh_enable()
- 硬中断、软中断、信号的区别:
- 硬中断是外部设备对CPU的中断
- 软中断是中断底半部的一种处理机制(和软件指令引发的中断不同,比如通过软中断陷入ARM内核)
- 信号是内核(或其他进程)对某个进程的中断,例如异步通知
- threaded_irq
- 内核可以通过以下函数代替request_irq()函数进行中断申请
- 该函数比request_irq()多一个参数thread_fn,该函数会为相应的中断号分配一个内核线程,该线程只针对这个中断号
- 参数handler对应的函数执行于中断上下文,thread_fn参数对应的函数则执行于内核线程。如果handler结束的时候,返回值是IRQ_WAKE_THREAD,内核会调度对应线程执行thread_fn对应的函数。
- 支持在irqflags中设置IRQF_ONESHOT标记,这样内核会自动帮助我们在中断上下文中屏蔽对应的中断号,而在内核调度thread_fn执行后,重新使能该中断号。
- handler参数可以设置为NULL,这种情况下,内核会用默认的irq_default_primary_handler()代替handler,并会使用IRQF_ONESHOT标记。
- 内核可以通过以下函数代替request_irq()函数进行中断申请
10.4 中断共享
- 使用方法:
- 申请中断时,应该使用IRQF_SHARED
- 申请中断时,最后一个参数传入设备结构体
- 中断到来时,会遍历执行共享中断的所有中断处理程序,知道某个函数返回IRQ_HANDLED,在顶半部中会根据硬件寄存器中的信息和dev参数进行判断是否为本地设备的中断,若不是,返回IRQ_NOTE。
- 编程要点
10.5 内核定时器
-
使用timer_list定义一个定时器:(重点成员变量:expires、function、data)
定义一个my_timer的定时器
1
struct
timer_list my_timer;
-
初始化timer:
1
void
init_timer(
struct
timer_list *timer)
-
使用宏对定时器成员变量赋值(也可像结构体一样赋值)
TIMER_INITIALIZER(_function, _expires, _data)
-
下宏可以定义并初始化定时器
DEFINE_TIMER(_name, _function, _expires, _data)
-
增加定时器、删除定时器、修改定时器的expire
1
2
3
void
add_timer(
struct
timer_list *timer);
int
del_timer(
struct
timer_list *timer);
int
mod_timer(
struct
timer_list *timer, unsigned
long
expires);
-
删除定时器时需要等待其处理完,因此del_timer()不能发生在中断上下文
- mod_timer用于修改定时器到期时间,新传入expires到来后才会执行定时器函数
- 如果修改时定时器还没有激活的话,mod_timer 函数会激活定时器
-
-
定时器使用模板
1
/* xxx设备结构体 */
2
struct
xxx_dev {
3
struct
cdev cdev;
4 ...
5 timer_list xxx_timer;
/* 设备要使用的定时器 */
6 };
7
8
/* xxx驱动中的某函数 */
9 xxx_func1(…)
10 {
11
struct
xxx_dev *dev = filp->private_data;
12 ...
13
/* 初始化定时器 */
14 init_timer(&dev->xxx_timer);
15 dev->xxx_timer.function = &xxx_do_timer;
16 dev->xxx_timer.data = (unsigned
long
)dev;
17
/* 设备结构体指针作为定时器处理函数参数 */
18 dev->xxx_timer.expires = jiffies + delay;
19
/* 添加(注册)定时器 */
20 add_timer(&dev->xxx_timer);
21 ...
22 }
23
24
/* xxx驱动中的某函数 */
25 xxx_func2(…)
26 {
27 ...
28
/* 删除定时器 */
29 del_timer (&dev->xxx_timer);
30 ...
31 }
32
33
/* 定时器处理函数 */
34
static
void
xxx_do_timer(unsigned
long
arg)
35 {
36
struct
xxx_device *dev = (
struct
xxx_device *)(arg);
37 ...
38
/* 调度定时器再执行 */
39 dev->xxx_timer.expires = jiffies + delay;
40 add_timer(&dev->xxx_timer);
41 ...
42 }
-
调度一个delayed_work在指定的延时后执行:
int
schedule_delayed_work(
struct
delayed_work *work, unsigned
long
delay)
-
取消delayed_work
1
2
int
cancel_delayed_work(
struct
delayed_work *work)
int
cancel_delayed_work_sync(
struct
delayed_work *work)
- 如果需要周期性执行任务,需要在delayed_work的工作函数中再次调用schedule_delayed_work()
10.6 内核延时
-
内核中的三个延时函数(忙等待类型):
1
2
3
void
ndelay(unsigned
long
nsecs);
//纳秒
void
udelay(usnigned
long
usecs);
//毫秒
void
mdelay(usnigned
long
msecs);
//微秒
-
在bootloader传递给内核的bootargs中设置lpj=1327104可以省略校准,节约百毫秒的开机时间
-
对毫秒级以上的延迟,建议用以下函数
1
2
3
void
msleep(unsigned
int
millisecs);
/*不可以被打断*/
unsigned
long
msleep_interruptible(unsigned
int
millisecs);
/*可以被打断*/
void
ssleep(unsigned
int
seconds);
/*不可以被打断*/
-
内核中延迟直观的方法是比较当前jiffies和目标jiffies,可通过以下函数实现
1
2
timer_before();
timer_after();
-
睡着延迟:等待的时间到来之前进程处于睡眠状态,CPU资源被其他进程使用
schedule_timeout()
/*当前任务休眠至指定的jiffies后重新被调度执行*/
-
将当前进程添加到等待队列中,从而在等待队列上睡眠,当超时发生时,进程将被唤醒(后者可以在超时前被打断)
1
2
sleep_on_timeout(wait_queue_head_t *q, unsigned
long
timeout)
interruptible_sleep_on_timeout(wait_queue_head_t *q, unsigned
long
timeout);