1 概述 ###############
由于中断服务程序的执行并不存在于进程上下文中,所以要求中断服务程序的时间要尽量短。因此, Linux在中断处理中引入了顶半部和底半部分离的机制。另外,内核对时钟的处理也采用中断方式,而内核软件定时器最终依赖于时钟中断。
根据中断的来源,中断可分为内部中断和外部中断
- 内部中断的中断源来自CPU内部(软件中断指令、溢出、除法错误等,例如,操作系统从用户态切换到内核态需借助CPU内部的软件中断)
- 外部中断的中断源来自CPU外部,由外设提出请求。
可屏蔽中断与不可屏蔽中断(NMI)
- 可屏蔽中断可以通过设置中断控制器寄存器等方法被屏蔽,屏蔽后,该中断不再得到响应
- 不可屏蔽中断不能被屏蔽
根据中断入口跳转方法的不同,中断可分为向量中断和非向量中断
- 采用向量中断的CPU通常为不同的中断分配不同的中断号,当检测到某中断号的中断到来后,就自动跳转到与该中断号对应的地址执行。不同中断号的中断有不同的入口地址。
- 非向量中断的多个中断共享一个入口地址,进入该入口地址后,再通过软件判断中断标志来识别具体是哪个中断。也就是说,向量中断由硬件提供中断服务程序入口地址,非向量中断由软件提供中断服务程序入口地址。
定时器在硬件上也依赖中断来实现,图10.1所示为典型的嵌入式微处理器内可编程间隔定时器(PIT)的工作原理,它接收一个时钟输入,当时钟脉冲到来时,将目前计数值增1并与预先设置的计数值(计数目标)比较,若相等,证明计数周期满,并产生定时器中断且复位目前计数值。
2 Linux中断处理程序架构 ###############
以往的中断带来的问题:
在大多数真实的系统中,当中断到来时,要完成的工作往往并不会是短小的,它可能要进行较大量的耗时处理。这时如果还用原来的中断类型,就降低CPU的利用率。
解决:
为了在中断执行时间尽量短和中断处理需完成的工作尽量大之间找到一个平衡点
Linux将中断处理程序分解为两个半部:顶半部(TopHalf)和底半部(Bottom Half)
顶半部用于完成尽量少的比较紧急的功能,它往往只是简单地读取寄存器中的中断状态,并在清除中断标志后就进行“登记中断”的工作。 “登记中断”意味着将底半部处理程序挂到该设备的底半部执行队列中去。这样,顶半部执行的速度就会很快,从而可以服务更多的中断请求。
- 顶半部:做的事紧急,做的事少,不能被打断,不耗时
- 底半部:做的事不紧急,做的事多,可以打断,耗时
查看/proc/interrupts文件可以获得系统中中断的统计信息,并能统计出每一个中断号上的中断在每个CPU上发生的次数。
CPU0 CPU1
0: 30 0 IO-APIC-edge timer
1: 62322 0 IO-APIC-edge i8042
6: 2 0 IO-APIC-edge floppy
8: 1 0 IO-APIC-edge rtc0
9: 0 0 IO-APIC-fasteoi acpi
12: 1959788 0 IO-APIC-edge i8042
14: 0 0 IO-APIC-edge ata_piix
15: 0 0 IO-APIC-edge ata_piix
16: 2190 0 IO-APIC-fasteoi vmwgfx, snd_ens1371
17: 639217 0 IO-APIC-fasteoi ehci_hcd:usb1, ioc0
18: 7920 0 IO-APIC-fasteoi uhci_hcd:usb2
19: 2082713 0 IO-APIC-fasteoi eth0
40: 0 0 PCI-MSI-edge PCIe PME, pciehp
41: 0 0 PCI-MSI-edge PCIe PME, pciehp
//省略-------------
72: 134183 0 PCI-MSI-edge ahci
73: 36890 0 PCI-MSI-edge vmw_vmci
74: 0 0 PCI-MSI-edge vmw_vmci
NMI: 0 0 Non-maskable interrupts
LOC: 5882566 6345827 Local timer interrupts
SPU: 0 0 Spurious interrupts
PMI: 0 0 Performance monitoring interrupts
IWI: 351047 354363 IRQ work interrupts
RTR: 0 0 APIC ICR read retries
RES: 4356189 5226162 Rescheduling interrupts
CAL: 466 572 Function call interrupts
TLB: 60686 43529 TLB shootdowns
TRM: 0 0 Thermal event interrupts
THR: 0 0 Threshold APIC interrupts
MCE: 0 0 Machine check exceptions
MCP: 917 917 Machine check polls
ERR: 0
MIS: 0
3 Linux中断编程 #############################
3.1 申请和释放中断 @@@@@@@@@@
在Linux设备驱动中,使用中断的设备需要申请和释放对应的中断,并分别使用内核提供的request_irq()和free_irq()函数。
1.申请irq
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev);
irq:中断号
handler:中断函数
//中断函数的返回值
enum irqreturn {
IRQ_NONE, //不是驱动所管理的设备产生的中断,用于共享中断
IRQ_HANDLED, //中断被正常处理
IRQ_WAKE_THREAD, //需要一个内核线程
};
flags:中断处理的属性,可以指定中断的触发方式以及处理方式。在触发方式方面,可以是
//上升沿
//下降沿
//高电平
//低电平
name:名字,可以在/proc/interrput/中找到
dev:作用挺大的,可以传递给中断函数一个参数,我们可以自己定义一个结构体,然后把一些要用到的数据传过去。
注意:如果没有用到共享中断,这个可以为NULL,但是该中断用到了共享中断,就必须赋值
devm_request_irq(struct device *dev, unsigned int irq, irq_handler_t handler,
unsigned long irqflags, const char *devname, void *dev_id)
此函数与request_irq()的区别是devm_开头的API申请的是内核“managed”的资源,一般不需要在出错处理和remove()接口里再显式的释放。有点类似Java的垃圾回收机制。
2.释放irq
irq:中断号
dev_id:传给中断函数的参数dev
void free_irq(unsigned int irq,void *dev_id);
3.2 使能和屏蔽中断 @@@@@@@@@@@
下列3个函数用于屏蔽一个中断源:
void disable_irq(int irq);
void disable_irq_nosync(int irq);
void enable_irq(int irq);
- disable_irq():会先等待指定的中断被处理完后,才去禁止该中断,因此如果在n号中断的顶半部调用disable_irq(n),会引起系统的死锁,这种情
- 况下,只能调用disable_irq_nosync(n)
- disable_irq_nosync():立即返回
下列两个函数(或宏,具体实现依赖于CPU的体系结构)将屏蔽本CPU内的所有中断:
#define local_irq_save(flags)
void local_irq_disable(void)
前者会将目前的中断状态保留在flags中(注意flags为unsigned long类型,被直接传递,而不是通过指针),后者直接禁止中断而不保存状态。
与上述两个禁止中断对应的恢复中断的函数(或宏)是:
#define local_irq_restore(flags)
void local_irq_enable(void)
3.3 底半部机制 @@@@@@@@@@@@@
Linux实现底半部的机制主要有tasklet、工作队列、软中断和线程化irq。
1.tasklet
tasklet的使用较简单,它的执行上下文是软中断,执行时机通常是顶半部返回的时候。我们只需要定义tasklet及其处理函数,并将两者关联则可,例如:
void my_tasklet_func(unsigned long); /*定义一个下半部处理函数*/
DECLARE_TASKLET(my_tasklet, my_tasklet_func, data);
/*定义一个tasklet结构my_tasklet,与my_tasklet_func(data)函数相关联*/
DECLARE_TASKLET
实现了定义名称为my_tasklet的tasklet,并将其与my_tasklet_func()这个函数绑定,而传入这个函数的参数为data。
tasklet_schedule()函数就能使系统在适当的时候调度下半部:
tasklet_schedule(&my_tasklet);
实例代码:
//bottom
void ket_bottom(unsigned long data)//下半部函数
{
printk("This is key_bottom function\n");
}
DECLARE_TASKLET(key_int, ket_bottom, 0);//定义下半部
//top
static irqreturn_t button_interrupt(int irq, void *dummy)
{
printk("interrupt top function\n");
tasklet_schedule(&key_int); //把下半部加入系统的调度队列
return IRQ_HANDLED;
}
2.工作队列(workqueue)
软中断和tasklet微线程,这两种方式归根结底都是采用软中断机制的,其根本上还是在中断的上下文中执行,所以这也就要求了采用这两种方式编写中断底半部,不能出现一些可能导致程序休眠或者是延迟的函数。
工作队列(workqueue)可以解决上面的问题。由于工作队列是工作在一个内核线程上,因此其工作环境为进程的上下文,从而工作函数可以休眠任意时间。
对于每一个通过队列,内核都会为其创建一个新的内核守护线程,也就是说,每一个工作队列都有唯一的一个内核线程与其对应。工作时,该内核线程就会轮询地执行这个工作队列上所有的工作节点上对应的处理函数。
workqueue_struct结构体
struct workqueue_struct {
//表示该workqueue_struct属于哪个CPU的队列
struct cpu_workqueue_struct *cpu_wq;
struct list_head list;//用来连接work_struct的队列头
const char *name;
int singlethread;
int freezeable; /* Freeze threads during suspend */
int rt;
struct lockdep_map lockdep_map;
};
与原来的tasklet一样,一个工作队列也是只能工作在一个CPU上面的,即每一个CPU都有一个工作队列。而cpu_workqueue_sruct就是描述该CPU的工作队列的结构体。
struct cpu_workqueue_struct {
struct global_cwq *gcwq; /* I: the associated gcwq */
struct workqueue_struct *wq; /* 指向属于该workqueue_struct结构体*/
int work_color; /* L: current color */
int flush_color; /* L: flushing color */
int nr_in_flight[WORK_NR_COLORS];/* L: nr of in_flight works */
int nr_active; /* L: nr of active works */
int max_active; /* L: max active works */
struct list_head delayed_works; /* L: delayed works */
};
工作队列最为重要的成员,work_struct。work_struct是工作队列里面的成员,里面会定义该work_struct的处理函数。
struct work_struct {
atomic_long_t data;
/* T if work item pending execution */
/* static initializer (debugobjects) */
struct list_head entry;
work_func_t func;
struct lockdep_map lockdep_map;
};
调度工作队列
int schedule_work(struct work_struct *work)
{
return queue_work(keventd_wq, work);
}/* 调度工作队列执行 */
schedule_work是将一个新的work_struct加入内核自己定义的keventd_wq(这个工作队列是已经被创建的系统工作队列)上。
用内核的的工作队列的缺点:当一调用上面这个函数会触发所有挂接在这上面的work_struct,浪费CPU时间。
我们在实际的使用过程中也可以直接使用queue_work()函数创建我们自己的工作队列。
创建工作queue wrok
方式一:
对应的销毁函数:
void destroy_workqueue(struct workqueue_struct *wq)
create_singlethread_workqueue对于多CPU系统,只负责创建一个worker_thread内核进程。
- 该内核进程被创建之后,会先定义一个图中的wait节点,然后在一循环体中检查cwq中的 worklist
- 如果该队列为空,那么就会把wait节点加入到cwq中的more_work中,然后休眠在该等待队列中
驱动调用queue_work向wq中加入工作节点。work会依次加在cwq->worklist所指向的链表中。queue_work向 cwq->worklist中加入一个work节点,同时会调用wake_up来唤醒休眠在cwq->more_work上的 worker_thread进程。wake_up会先调用wait节点上的autoremove_wake_function函数,然后将wait节点从 cwq->more_work中移走。
worker_thread再次被调度,开始处理cwq->worklist中的所有work节点...当所有work节点处理完 毕,worker_thread重新将wait节点加入到cwq->more_work,然后再次休眠在该等待队列中直到Driver调用 queue_work...
方式二:
相对于create_singlethread_workqueue, create_workqueue同样会分配一个wq的工作队列,但是不同之处在于,对于多CPU系统而言,对每一个CPU,都会为之创建一个per- CPU的cwq结构,对应每一个cwq,都会生成一个新的worker_thread进程。但是当用queue_work向cwq上提交work节点时, 是哪个CPU调用该函数,那么便向该CPU对应的cwq上的worklist上增加work节点。
小结
当用户调用workqueue的初始化接口create_workqueue或者create_singlethread_workqueue对 workqueue队列进行初始化时,内核就开始为用户分配一个workqueue对象,并且将其链到一个全局的workqueue队列中。
然后 Linux根据当前CPU的情况,为workqueue对象分配与CPU个数相同的cpu_workqueue_struct对象,每个 cpu_workqueue_struct对象都会存在一条任务队列。
紧接着,Linux为每个cpu_workqueue_struct对象分配一个内 核thread,即内核daemon去处理每个队列中的任务。
至此,用户调用初始化接口将workqueue初始化完毕,返回workqueue的指针。
workqueue初始化完毕之后,将任务运行的上下文环境构建起来了,但是具体还没有可执行的任务,所以,需要定义具体的work_struct对象。然后将work_struct加入到任务队列中,Linux会唤醒daemon去处理任务。
上述描述的workqueue内核实现原理可以描述如下:
work queue API
1. create_workqueue用于创建一个workqueue队列,为系统中的每个CPU都创建一个内核线程。输入参数:
@name:workqueue的名称
2. create_singlethread_workqueue用于创建workqueue,只创建一个内核线程。输入参数:
@name:workqueue名称
3. destroy_workqueue释放workqueue队列。输入参数:
@ workqueue_struct:需要释放的workqueue队列指针
4. schedule_work调度执行一个具体的任务,执行的任务将会被挂入Linux系统提供的workqueue——keventd_wq输入参数:
@ work_struct:具体任务对象指针
5. schedule_delayed_work延迟一定时间去执行一个具体的任务,功能与schedule_work类似,多了一个延迟时间,输入参数:
@work_struct:具体任务对象指针
@delay:延迟时间
6. queue_work调度执行一个指定workqueue中的任务。输入参数:
@ workqueue_struct:指定的workqueue指针
@work_struct:具体任务对象指针
7. queue_delayed_work延迟调度执行一个指定workqueue中的任务,功能与queue_work类似,输入参数多了一个delay。
实例代码:
struct work_struct work; //工作队列
struct workqueue_struct *wq; //工作节点
static void fun_worker(struct work_struct *work) //工作节点函数
wq = create_singlethread_workqueue("kworkqueue_ts");//创建工作队列
flush_workqueue(&wq);//清洗
INIT_WORK(&work, fun_worker);//初始化节点
//自己定义的中断函数
static irqreturn_t fun_interrupt(int irq, void *dummy)
{
if (!work_pending(&work))
{
queue_work(wq, &work);//调用工作节点
}
return IRQ_HANDLED;
}
3.软中断
软中断(Softirq)也是一种传统的底半部处理机制,它的执行时机通常是顶半部返回的时候, tasklet是基于软中断实现的,因此也运行于软中断上下文。
softirq_action结构体表征一个软中断,这个结构体包含软中断处理函数指针和传递给该函数的参数。
struct softirq_action
{
void (*action)(struct softirq_action *);
};
open_softirq()注册软中断对应的处理函数
raise_softirq()函数可以触发一个软中断
local_bh_disable()和local_bh_enable()是内核中用于禁止和使能软中断及tasklet底半部机制的函数。
内核中采用softirq的地方包括HI_SOFTIRQ、TIMER_SOFTIRQ、 NET_TX_SOFTIRQ、NET_RX_SOFTIRQ、 SCSI_SOFTIRQ、 TASKLET_SOFTIRQ等。
一般来说,驱动的编写者不会也不宜直接使用softirq。
中断,软中断,tasklet和工作队列的区别:
硬中断是外部设备对CPU的中断
软中断是中断底半部的一种处理机制
中断,软中断和tasklet都是工作在中断上下文,不能直接或间接调用调度器。
工作队列是进程上下文,可以调用调度器。
需要特别说明的是,软中断以及基于软中断的tasklet如果在某段时间内大量出现的话,内核会把后续软中断放入ksoftirqd内核线程中执行。总的来说,中断优先级高于软中断,软中断又高于任何一个线程。软中断适度线程化,可以缓解高负载情况下系统的响应。
4.threaded_irq
在内核中,除了可以通过request_irq()、devm_request_irq()申请中断以外,
还可以通过request_threaded_irq()和devm_request_threaded_irq()申请。这两个函数的原型为
request_threaded_irq(unsigned int irq, irq_handler_t handler,
irq_handler_t thread_fn,
unsigned long flags, const char *name, void *dev);
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);
由此可见,它们比request_irq()、 devm_request_irq()多了一个参数thread_fn。用这两个API申请中断的时候,内核会为相应的中断号分配一个对应的内核线程。注意这个线程只针对这个中断号,如果其他中断也通过request_threaded_irq()申请,自然会得到新的内核线程。
参数handler对应的函数执行于中断上下文, thread_fn参数对应的函数则执行于内核线程。如果handler结束的时候,返回值是IRQ_WAKE_THREAD,内核会调度对应线程执行thread_fn对应的函数。
request_threaded_irq()和devm_request_threaded_irq()支持在irqflags中设置IRQF_ONESHOT标记,这样内核会自动帮助我们在中断上下文中屏蔽对应的中断号,而在内核调度thread_fn执行后,重新使能该中断号。对于我们无法在上半部清除中断的情况, IRQF_ONESHOT特别有用,避免了中断服务程序一退出,中断就洪泛的情况。
handler参数可以设置为NULL,这种情况下,内核会用默认的irq_default_primary_handler()代替handler,并会使用
IRQF_ONESHOT标记。 irq_default_primary_handler()定义为:
实例代码:
drivers/input/keyboard/gpio_keys.c是一个放之四海皆准的GPIO按键驱动,为了让该驱动在特定的电路板上工作,通常
只需要修改arch/arm/mach-xxx下的板文件或者修改device tree对应的dts。该驱动会为每个GPIO申请中断,在gpio_keys_setup_key()函数中进行。注意最后一个参数bdata,会被传入中断服务程序
4 中断共享 #################
多个设备共享一根硬件中断线的情况在实际的硬件系统中广泛存在, Linux支持这种中断共享。下面是中断共享的使用方法。
1)共享中断的多个设备在申请中断时,都应该使用IRQF_SHARED标志,而且一个设备以IRQF_SHARED申请某中断成功的前提是该中断未被申请,或该中断虽然被申请了,但是之前申请该中断的所有设备也都以IRQF_SHARED标志申请该中断。
2)尽管内核模块可访问的全局地址都可以作为request_irq(…, void*dev_id)的最后一个参数dev_id,但是设备结构体指针显然是可传入的最佳参数。
3)在中断到来时,会遍历执行共享此中断的所有中断处理程序,直到某一个函数返回IRQ_HANDLED。在中断处理程序顶半部中,应根据硬件寄存器中的信息比照传入的dev_id参数迅速地判断是否为本设备的中断,若不是,应迅速返回IRQ_NONE,如图10.5所示。
/*
* These flags used only by the kernel as part of the
* irq handling routines.
*
* IRQF_DISABLED - keep irqs disabled when calling the action handler.
* DEPRECATED. This flag is a NOOP and scheduled to be removed
* IRQF_SAMPLE_RANDOM - irq is used to feed the random generator
* IRQF_SHARED - allow sharing the irq among several devices
* IRQF_PROBE_SHARED - set by callers when they expect sharing mismatches to occur
* IRQF_TIMER - Flag to mark this interrupt as timer interrupt
* IRQF_PERCPU - Interrupt is per cpu
* IRQF_NOBALANCING - Flag to exclude this interrupt from irq balancing
* IRQF_IRQPOLL - Interrupt is used for polling (only the interrupt that is
* registered first in an shared interrupt is considered for
* performance reasons)
* IRQF_ONESHOT - Interrupt is not reenabled after the hardirq handler finished.
* Used by threaded interrupts which need to keep the
* irq line disabled until the threaded handler has been run.
* IRQF_NO_SUSPEND - Do not disable this IRQ during suspend
*
*/
实例代码:共享中断必须设置 IRQF_SHARED
/* 中断处理顶半部 */
irqreturn_t xxx_interrupt(int irq, void *dev_id)
{
//...
int status = read_int_status(); /* 获知中断源 */
if(!is_myint(dev_id,status)) /* 判断是否为本设备中断 */
{
return IRQ_NONE; /* 不是本设备中断,立即返回 */
}
/* 是本设备中断,进行处理 */
//...
return IRQ_HANDLED; /* 返回IRQ_HANDLED表明中断已被处理*/
}
/* 设备驱动模块加载函数 */
int xxx_init(void)
{
/* 申请共享中断 */
result = request_irq(sh_irq, xxx_interrupt, IRQF_SHARED, "xxx", xxx_dev);
//...
}
/* 设备驱动模块卸载函数 */
void xxx_exit(void)
{
//...
/* 释放中断 */
free_irq(xxx_irq, xxx_interrupt);
//...
}
总结:
软中断和tasklet运行于软中断上下文,仍然属于原子上下文的一种,而工作队列则运行于进程上下文。
因此,在软中断和tasklet处理函数中不允许睡眠,而在工作队列处理函数中允许睡眠。
但是都可以被其他中断顶半部打断
下面的方法也可以判断是哪一个中断
reg = read_register(中断状态寄存器);
/* 表示判断外部中断状态寄存器的第18号中断的挂起标志位,0x+++++表示EINT_18的状态位为1,其它为0 */
if(!(reg & 0x+++++))
{
/* 不是,则返回该标号 */
rerurn IRQ_NONE;
}
5 内核定时器 #####################
5.1 内核定时器编程 @@@@@@@@@@
软件意义上的定时器最终依赖硬件定时器来实现,内核在时钟中断发生后检测各定时器是否到期,到期后的定时器处理函数将作为软中断在底半部执行。实质上,时钟中断处理程序会唤起TIMER_SOFTIRQ软中断,运行当前处理器上到期的所
有定时器。
在Linux设备驱动编程中,可以利用Linux内核中提供的一组函数和数据结构来完成定时触发工作或者完成某周期性的事务。这组函数和数据结构使得驱动工程师在多数情况下不用关心具体的软件定时器究竟对应着怎样的内核和硬件行为。
1 低精度定时器
1.timer_list
struct timer_list {
/*
* All fields that change during normal runtime grouped to the
* same cacheline
*/
struct list_head entry;//内核使用
unsigned long expires; //设定的超时的值
struct tvec_base *base;//内核使用
void (*function)(unsigned long);//定时中断函数
unsigned long data; //传入函数的参数
int slack;
};
2.初始化定时器
初始化:init_timer()
void init_timer_key(struct timer_list *timer,
const char *name,
struct lock_class_key *key);
用定义结构的快捷方式:DEFINE_TIMER(_name, _function, _expires, _data)
此外, setup_timer()也可用于初始化定时器并赋值其成员,其源代码为:
3.增加定时器
void add_timer(struct timer_list *timer)
{
BUG_ON(timer_pending(timer));
mod_timer(timer, timer->expires);
}
4.删除定时器
int del_timer(struct timer_list * timer);
del_timer_sync()是del_timer()的同步版,在删除一个定时器时需等待其被处理完,因此该函数的调用不能发生在中断上下文中
int del_timer_sync(struct timer_list *timer);
5.修改定时器的expire
int mod_timer(struct timer_list *timer, unsigned long expires);
上述函数用于修改定时器的到期时间,在新的被传入的expires到来后才会执行定时器函数。
实例代码:
/* xxx设备结构体 */
struct xxx_dev {
struct cdev cdev;
timer_list xxx_timer; /* 设备要使用的定时器 */
};
/* xxx驱动中的某函数 */
xxx_func1(…)
{
struct xxx_dev *dev = filp->private_data;
/* 初始化定时器 */
init_timer(&dev->xxx_timer);
dev->xxx_timer.function = &xxx_do_timer;
dev->xxx_timer.data = (unsigned long)dev;
/* 设备结构体指针作为定时器处理函数参数 */
dev->xxx_timer.expires = jiffies + delay;
/* 添加(注册)定时器 */
add_timer(&dev->xxx_timer);
}
/* xxx驱动中的某函数 */
xxx_func2(…)
{
/* 删除定时器 */
del_timer (&dev->xxx_timer);
}
/* 定时器处理函数 */
static void xxx_do_timer(unsigned long arg)
{
struct xxx_device *dev = (struct xxx_device *)(arg);
/* 调度定时器再执行 */
dev->xxx_timer.expires = jiffies + delay;
add_timer(&dev->xxx_timer);
}
注意:定时器的到期时间往往是在目前jiffies的基础上添加一个时延,若为1Hz,则表示延迟1s。
jiffies为系统的值
Linux内核中一个重要的全局变量是HZ,这个变量表示与时钟中断相关的一个值。时钟中断是由系统定时硬件以周期性的间隔产生,这个周期性的值由HZ来表示。根据不同的硬件平台, Hz的取值是不一样的。这个值一般被定义为1000,如下代码所示。
这里Hz的意思是每一秒钟时钟中断发生1000次。每当时钟中断发生时,内核内部计数器的值就会加上1。内部计数器由jiffies变量来表示,当系统初始化时,这个变量被设置为0,每一个时钟到来时,这个计数器的值加1,也就是说这个变量记录了系统引导以来经历的时间。
小技巧:
在定时器处理函数中,在完成相应的工作后,往往会延后expires并将定时器再次添加到内核定时器链表中,以便定时器能再次被触发。
2 高精度定时器
因为低精度定时器以jiffies来定时,所以定时精度受系统的HZ影响,而通常这个值都不高。如果HZ的值为200.那么一个jiffy的时间就是5毫秒,也就是说。定时器的精度是5毫秒。
对于像声卡一样的设备,是需要高精度的定时器。
高精度是以下面的共用体来定义:
union ktime {
s64 tv64; //64位定时
struct {
s32 sec, nsec;
s32 nsec, sec; //32位定时
} tv;
};
typedef union ktime ktime_t; /* Kill this */
设置定时时间:如 ktime_t my_time ktime_set(1, 2)
static inline ktime_t ktime_set(const long secs, const unsigned long nsecs)//秒和纳秒
{
if (unlikely(secs >= KTIME_SEC_MAX))
return (ktime_t){ .tv64 = KTIME_MAX };
return (ktime_t) { .tv64 = (s64)secs * NSEC_PER_SEC + (s64)nsecs };
}
定时器的结构体类型:
struct hrtimer {
struct rb_node node;
ktime_t _expires;
ktime_t _softexpires;
enum hrtimer_restart (*function)(struct hrtimer *);
struct hrtimer_clock_base *base;
unsigned long state;
int start_pid;
void *start_site;
char start_comm[16];
};
关键函数:
void hrtimer_init(struct hrtimer *timer, clockid_t clock_id,
enum hrtimer_mode mode)
作用:初始化
clock_id:时钟的类型,常用的是CLOCK_MONOTONIC,表示系统开机以来的时间
mode:时间的模式,
enum hrtimer_mode {
HRTIMER_MODE_ABS = 0x0, /* 绝对时间 */
HRTIMER_MODE_REL = 0x1, /* 相对时间 */
HRTIMER_MODE_PINNED = 0x02, /* Timer is bound to CPU */
HRTIMER_MODE_ABS_PINNED = 0x02,
HRTIMER_MODE_REL_PINNED = 0x03,
};
int hrtimer_start(struct hrtimer *timer, ktime_t tim, const enum hrtimer_mode mode)
作用:启动定时器
tim:是设定的到期时间
static inline u64 hrtimer_forward_now(struct hrtimer *timer,
ktime_t interval)
作用:以现在位起点重新设置到期的定时值
int hrtimer_cancel(struct hrtimer *timer)
作用:取消定时器
实例:
hrtimer_init(&timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL); //初始化
timer.function = timer_func;
hrtimer_start(&kp->timer, ktime_set(0, 0), HRTIMER_MODE_REL);
static enum hrtimer_restart timer_func(struct hrtimer *timer)
5.2 内核中延迟的工作delayed_work @@@@@@@@@@@@@@
对于周期性的任务,除了定时器以外,在Linux内核中还可以利用一套封装得很好的快捷机制,其本质是利用工作队列和定时器实现,这套快捷机制就是delayed_work,
delayed_work结构体的定义
struct delayed_work {
struct work_struct work;
struct timer_list timer;
};
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func;
};
struct timer_list {
struct list_head entry;
unsigned long expires;
struct tvec_base *base;
void (*function)(unsigned long);
unsigned long data;
int slack;
};
我们可以通过如下函数调度一个delayed_work在指定的延时后执行:
int schedule_delayed_work(struct delayed_work *dwork,
unsigned long delay)
{
return queue_delayed_work(keventd_wq, dwork, delay);
}
当指定的delay到来时, delayed_work结构体中的work成员work_func_t类型成员func()会被执行。 work_func_t类型定义
为:
typedef void (*work_func_t)(struct work_struct *work);
其中, delay参数的单位是jiffies,因此一种常见的用法如下:
schedule_delayed_work(&work, msecs_to_jiffies(poll_interval));
msecs_to_jiffies()用于将毫秒转化为jiffies
如果要周期性地执行任务,通常会在delayed_work的工作函数中再次调用schedule_delayed_work(),周而复始。
如下函数用来取消delayed_work
int cancel_delayed_work(struct delayed_work *work);
int cancel_delayed_work_sync(struct delayed_work *work);
5.3 实例:秒字符设备 @@@@@@@@
下面我们编写一个字符设备“second”(即“秒”)的驱动,它在被打开的时候初始化一个定时器并将其添加到内核定时器链表中,每秒输出一次当前的jiffies(为此,定时器处理函数中每次都要修改新的expires),整个程序如下
static int second_major = SECOND_MAJOR;
module_param(second_major, int, S_IRUGO);
struct second_dev {
struct cdev cdev;
atomic_t counter;
struct timer_list s_timer;
};
static struct second_dev *second_devp;
static void second_timer_handler(unsigned long arg)
{
mod_timer(&second_devp->s_timer, jiffies + HZ); /* 触发下一次定时
atomic_inc(&second_devp->counter); /* 增加秒计数 */
printk(KERN_INFO "current jiffies is %ld\n", jiffies);
}
static int second_open(struct inode *inode, struct file *filp)
{
init_timer(&second_devp->s_timer);
second_devp->s_timer.function = &second_timer_handler;
second_devp->s_timer.expires = jiffies + HZ;
add_timer(&second_devp->s_timer);
atomic_set(&second_devp->counter, 0); /* 初始化秒计数为 */
return 0;
}
static int second_release(struct inode *inode, struct file *filp)
{
del_timer(&second_devp->s_timer);
return 0;
}
static ssize_t second_read(struct file *filp, char __user * buf, size
loff_t * ppos)
{
int counter;
counter = atomic_read(&second_devp->counter);
if (put_user(counter, (int *)buf))/* 复制counter到userspace */
return -EFAULT;
else
return sizeof(unsigned int);
}
static const struct file_operations second_fops = {
.owner = THIS_MODULE,
.open = second_open,
.release = second_release,
.read = second_read,
};
static void second_setup_cdev(struct second_dev *dev, int index)
{
int err, devno = MKDEV(second_major, index);
cdev_init(&dev->cdev, &second_fops);
dev->cdev.owner = THIS_MODULE;
err = cdev_add(&dev->cdev, devno, 1);
if (err)
printk(KERN_ERR "Failed to add second device\n");
}
static int __init second_init(void)
{
int ret;
dev_t devno = MKDEV(second_major, 0);
if (second_major)
ret = register_chrdev_region(devno, 1, "second");
else {
ret = alloc_chrdev_region(&devno, 0, 1, "second");
second_major = MAJOR(devno);
}
if (ret < 0)
return ret;
second_devp = kzalloc(sizeof(*second_devp), GFP_KERNEL);
if (!second_devp) {
ret = -ENOMEM;
goto fail_malloc;
}
second_setup_cdev(second_devp, 0);
return 0;
fail_malloc:
unregister_chrdev_region(devno, 1);
return ret;
}
module_init(second_init);
static void __exit second_exit(void)
{
cdev_del(&second_devp->cdev);
kfree(second_devp);
unregister_chrdev_region(MKDEV(second_major, 0), 1);
}
module_exit(second_exit);
MODULE_AUTHOR("Barry Song <21cnbao@gmail.com>");
MODULE_LICENSE("GPL v2");
用户空间的代码:
include ...
2
3main()
4{
5 int fd;
6 int counter = 0;
7 int old_counter = 0;
8
9 /* 打开/dev/second设备文件 */
10 fd = open("/dev/second", O_RDONLY);
11 if (fd != - 1) {
13 while (1) {
15 read(fd,&counter, sizeof(unsigned int));/* 读目前经历的秒数*/
16 if(counter!=old_counter) {
18 printf("seconds after open /dev/second :%d\n",counter);
19 old_counter = counter;
20 }
21 }
22 } else {
25 printf("Device open failure\n");
26 }
}
运行second_test后,内核将不断地输出目前的jiffies值:
[13935.122093] current jiffies is 13635122
[13936.124441] current jiffies is 13636124
[13937.126078] current jiffies is 13637126
6 内核延时 ###############
1 短延迟 @@@@@@@@
Linux内核中提供了下列3个函数以分别进行纳秒、微秒和毫秒延迟
void ndelay(unsigned long nsecs);
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);
毫秒以上的时延,最好不要直接使用mdelay()函数,这将耗费CPU资源,
对于毫秒级以上的时延,内核提供了下述函数
void msleep(unsigned int millisecs);
unsigned long msleep_interruptible(unsigned int millisecs);
void ssleep(unsigned int seconds);
msleep()、 ssleep()不能被打断,而msleep_interruptible()则可以被打断。
2 长延迟 @@@@@@@@@@@
在内核中进行延迟的一个很直观的方法是比较当前的jiffies和目标jiffies(设置为当前jiffies加上时间间隔的
jiffies),直到未来的jiffies达到目标jiffies。代码给出了使用忙等待先延迟100个jiffies再延迟2s的实例。
/* 延迟100个jiffies */
unsigned long delay = jiffies + 100;
while(time_before(jiffies, delay));
/* 再延迟2s */
unsigned long delay = jiffies + 2*Hz;
while(time_before(jiffies, delay));
与time_before()对应的还有一个time_after(),它们在内核中定义为(实际上只是将传入的未来时间jiffies和被调用时的jiffies进行一个简单的比较):
为了防止在time_before()和time_after()的比较过程中编译器对jiffies的优化,内核将其定义为volatile变量,这将保证每次都会重新读取这个变量。因此volatile更多的作用还是避免这种读合并。
3 睡着延迟 @@@@@@@@@@
睡着延迟无疑是比忙等待更好的方式,睡着延迟是在等待的时间到来之前进程处于睡眠状态, CPU资源被其他进程使
用。 schedule_timeout()可以使当前任务休眠至指定的jiffies之后再重新被调度执行, msleep()和msleep_interruptible()
在本质上都是依靠包含了schedule_timeout()的schedule_timeout_uninterruptible()和schedule_timeout_interruptible()来实现的,如代码清单10.16
所示。
void msleep(unsigned int msecs)
{
unsigned long timeout = msecs_to_jiffies(msecs) + 1;
while (timeout)
timeout = schedule_timeout_uninterruptible(timeout);
}
/**
* msleep_interruptible - sleep waiting for signals
* @msecs: Time in milliseconds to sleep for
*/
unsigned long msleep_interruptible(unsigned int msecs)
{
unsigned long timeout = msecs_to_jiffies(msecs) + 1;
while (timeout && !signal_pending(current))
timeout = schedule_timeout_interruptible(timeout);
return jiffies_to_msecs(timeout);
}
总结:
Linux的中断处理分为两个半部,顶半部处理紧急的硬件操作,底半部处理不紧急的耗时操作。 tasklet和工作队列都是调度中断底半部的良好机制, tasklet基于软中断实现。内核定时器也依靠软中断实现。
内核中的延时可以采用忙等待或睡眠等待,为了充分利用CPU资源,使系统有更好的吞吐性能,在对延迟时间的要求并不是很精确的情况下,睡眠等待通常是值得推荐的,而ndelay()、 udelay()忙等待机制在驱动中通常是为了配合硬件上的短时延迟要求