本文从裸机,实时操作系统,Linux操作系统三个层面分析嵌入式开发中的“时间管理”。
裸机开发:以stm32固件库为例
裸机开发时,针对时间的管理,大多使用外设——定时器(以基本定时器为例,不涉及高级定时器内容)
定时器的初始化结构体:
typedef struct {
uint16_t TIM_Prescaler; // 预分频器
uint16_t TIM_CounterMode; // 计数模式
uint32_t TIM_Period; // 定时器周期
uint16_t TIM_ClockDivision;// 时钟分频
uint8_t TIM_RepetitionCounter; // 重复计算器
} TIM_TimeBaseInitTypeDef;
编程步骤:
(1) 开定时器时钟
(2) 初始化时基初始化结构体;
(3) 使能定时器中断;
(4) 打开定时器;
(5) 编写中断服务程序
定时器实现机制:
定义一个全局变量time,让 time 来记录进入中断的次数。在中断服务函数中,每经过一次定时器中断就对time的值+1,我们只需要判断time*(一次定时器中断的时间)的大小,就能判断时间。(在中断服务程序的最后,要把相应的中断标志位清除掉,切记。)
void BASIC_TIM_IRQHandler(void)
{
if (TIM_GetITStatus( BASIC_TIM, TIM_IT_Update) != RESET ) {
time++;
TIM_ClearITPendingBit(BASIC_TIM, TIM_FLAG_Update);
}
}
实时操作系统:以RT-thread为例
定时器分类:硬件定时器,软件定时器
硬件定时器:芯片本身提供的定时功能。一般是由外部晶振提供给芯片输入时钟,芯片向软件模块提供一组配置寄存器,接受控制输入,到达设定时间值后芯片中断控制器产生时钟中断。硬件定时器的精度一般很高,可以达到纳秒级别,并且是中断触发方式。
软件定时器:软件定时器是由操作系统提供的一类系统接口,它构建在硬件定时器基础之上,使系统能够提供不受硬件定时器资源限制的定时器服务,它实现的功能与硬件定时器也是类似的。
(不难看出前文裸机开发中,我们用的是硬件定时器。)
RT-Thread 操作系统提供软件定时器功能,软件定时器的使用相当于扩展了定时器的数量,允许创建更多的定时业务。RT-Thread 软件定时器功能上支持:
l 静态裁剪:能通过宏关闭软件定时器功能。
l 软件定时器创建。
l 软件定时器启动。
l 软件定时器停止。
l 软件定时器删除
软件定时器模式:单次模式和周期模式
单次模式:时间到达后,执行回调函数,就将定时器删除。
周期模式:定时器会按照设置的定时时间循环执行超时函数,直到用户将定时器删除
软件、硬件定时器的差异:
精度上,硬件定时器更高
数量上,软件定时器不受数量限制
另外,硬件定时器超时函数的上下文环境是中断,而软件定时器超时函数的上下文是线程,所以软件定时器的定时过程中是极有可能被其它的线程所打断的。
(RT-Thread 定时器默认的方式是 HARD_TIMER 模式,即定时器超时后,超时函数是在系统时钟中断的上下文环境中运行的。在中断上下文中的执行方式决定了定时器的超时函数不应该调用任何会让当前上下文挂起的系统函数;也不能够执行非常长的时间,否则会导致其他中断的响应时间加长或抢占了其他线程执行的时间。
SOFT_TIMER 模式可配置,通过宏定义 RT_USING_TIMER_SOFT 来决定是否启用该模式。该模式被启用后,系统会在初始化时创建一个timer 线程,然后 SOFT_TIMER 模式的定时器超时函数在都会在timer 线程的上下文环境中执行。可以在初始化 / 创建定时器时使用参数 RT_TIMER_FLAG_SOFT_TIMER来指定设置 SOFT_TIMER 模式。)
软件定时器实现机制
时钟节拍:
RT-thread定时器基于时钟节拍,既操作系统中最小的时间单位。时钟节拍由配置为中断触发模式的硬件定时器产生。
void SysTick_Handler(void)
{
/*进 入 中 断 */
rt_interrupt_enter();
……
rt_tick_increase();
/*退 出 中 断 */
rt_interrupt_leave();
}
在中断函数中调用rt_tick_increase() 对全局变量 rt_tick 进行自加
void rt_tick_increase(void)
{
struct rt_thread *thread;
/* 全 局 变 量 rt_tick 自 加 */
++ rt_tick;
/* 检 查 时 间 片 */
thread = rt_thread_self();
--thread->remaining_tick;
if (thread->remaining_tick== 0)
{
/* 重 新 赋 初 值 */
thread->remaining_tick = thread->init_tick;
/* 线 程 挂 起 */
rt_thread_yield();
}
/* 检 查 定 时 器 */
rt_timer_check();
}
可以看到全局变量 rt_tick 在每经过一个时钟节拍时,值就会加 1, rt_tick 的值表示了系统从启动开始总共经过的时钟节拍数,即系统时间。此外,每经过一个时钟节拍时,都会检查当前线程的时间片是否用完,以及是否有定时器超时。
获取时钟节拍:
rt_tick_t rt_tick_get(void);
返回: rt_tick
描述: 当前时钟节拍值
在 RT-Thread 定时器模块中维护着两个重要的全局变量:
(1)当前系统经过的 tick 时间 rt_tick(当硬件定时器中断来临时,它将加 1);
(2)定时器链表rt_timer_list。系统新创建并激活的定时器都会按照以超时时间排序的方式插入到rt_timer_list链表中。
在 RT-Thread 操作系统中,定时器控制块由结构体 struct rt_timer 定义并形成定时器内核对象,再链接到内核对象容器中进行管理。它是操作系统用于管理定时器的一个数据结构,会存储定时器的一些信息,例如初始节拍数,超时时的节拍数,也包含定时器与定时器之间连接用的链表结构,超时回调函数等。
struct rt_timer
{
struct rt_object parent;
rt_list_t row[RT_TIMER_SKIP_LIST_LEVEL]; /*定 时 器 链 表 节 点*/
void (*timeout_func)(void *parameter); /* 定时 器 超 时 调 用 的 函 数*/
void *parameter; /* 超 时 函 数 的 参 数*/
rt_tick_t init_tick; /* 定 时 器 初 始 超 时节 拍 数*/
rt_tick_t timeout_tick; /* 定 时 器 实 际超 时 时 的 节 拍 数*/
};
typedef struct rt_timer *rt_timer_t;
定时器控制块由struct rt_timer 结构体定义并形成定时器内核对象,再链接到内核对象容器中进行管理, list成员则用于把一个激活的(已经启动的)定时器链接到rt_timer_list 链表中。
定时器的管理方式:
在系统启动时需要初始化定时器管理系统。可以通过下面的函数接口完成:
void rt_system_timer_init(void);
如果需要使用SOFT_TIMER,则系统初始化时,应该调用下面这个函数接口:
void rt_system_timer_thread_init(void);
创建和删除定时器:
rt_timer_t rt_timer_create( const char* name,
void(*timeout)(void* parameter),
void* parameter,
rt_tick_t time,
rt_uint8_t flag );
参数 描述
name 定时器的名称
void (timeout) (void parameter) 定时器超时函数指针(当定时器超时时,系统 会调用这个函数)parameter 定时器超时函数的入 口参数(当定时器超时时,调用超时回调函数会 把这个参数做为入口参数传递给超时函数)
time 定时器的超时时间,单位是时钟节拍flag 定时器 创建时的参数,支持的值包括单次定时、周期定 时、硬件定时器、软件定时器等(可以用 “或” 关 系取多个值)
返回 ——
RT_NULL 创建失败(通常会由于系统内存不够用而返回 RT_NULL)
定时器的句柄 定时器创建成功
rt_err_t rt_timer_delete(rt_timer_t timer);
参数 描述
timer 定时器句柄 指向要删除的定时器
返回 ——
RT_EOK 删除成功(如果参数timer 句柄是一个 RT_NULL,将会导致一个ASSERT 断言)
初始化和脱离定时器:
当选择静态创建定时器时,可利用rt_timer_init 接口来初始化该定时器,函数接口如下:
void rt_timer_init(rt_timer_t timer,
const char* name,
void (*timeout)(void* parameter),
void* parameter,
rt_tick_t time,
rt_uint8_t flag);
参数 描述
timer 定时器句柄,指向要初始化的定时器控制块
name 定时器的名称
void (timeout) (void parameter) 定时器超时函数指针(当定时器超时时,系统 会调用这个函数)
parameter 定时器超时函数的入口参数(当定时器超时时, 调用超时回调函数会把这个参数做为入口参数传 递给超时函数)
time 定时器的超时时间,单位是时钟节拍
flag 定时器创建时的参数,支持的值包括单次定时、 周期定时、硬件定时器、软件定时器
当一个静态定时器不需要再使用时,可以使用下面的函数接口:
rt_err_t rt_timer_detach(rt_timer_t timer);
脱离定时器时,系统会把定时器对象从内核对象容器中脱离,但是定时器对象所占有的内存不会被释放
参数 描述
timer 定时器句柄 指向要脱离的定时器控制块
返回 ——
RT_EOK 脱离成功
启动和停止定时器:
当定时器被创建或者初始化以后,并不会被立即启动,必须在调用启动定时器函数接口后,才开始工作,启动定时器函数接口如下:
rt_err_t rt_timer_start(rt_timer_t timer);
调用定时器启动函数接口后,定时器的状态将更改为激活状(RT_TIMER_FLAG_ACTIVATED),并按照超时顺序插入到 rt_timer_list队列链表中
参数 描述
timer 定时器句柄 指向要启动的定时器控制块
返回 ——
RT_EOK 启动成功
启动定时器以后,若想使它停止,可以使用下面的函数接口:
rt_err_t rt_timer_stop(rt_timer_t timer);
调用定时器停止函数接口后,定时器状态将更改为停止状态,并从rt_timer_list 链表中脱离出来不参与定时器超时检查。当一个(周期性)定时器超时时,也可以调用这个函数接口停止这个(周期性)定时器本身
参数 描述
timer 定时器句柄 指向要停止的定时器控制块
返回 ——
RT_EOK 成功停止定时器
-RT_ERROR timer 已经处于停止状态
控制定时器:
除了上述提供的一些编程接口,RT-Thread 也额外提供了定时器控制函数接口,以获取或设置更多定时器的信息。控制定时器函数接口如下:
rt_err_t rt_timer_control(rt_timer_t timer, rt_uint8_t cmd, void* arg);
控制定时器函数接口可根据命令类型参数,来查看或改变定时器的设置,其中的各参数和返回值说明详见下表:
参数 描述
timer 定时器句柄,指向要停止的定时器控制块
cmd 用于控制定时器的命令,当前支持四个命 令,分别是设置定时时间,查看定时时间, 设置单次触发,设置周期触发
arg 与 cmd 相对应的控制命令参数比如, cmd 为设定超时时间时,就可以将超时时间参数 通过arg 进行设定
返回 ——
RT_EOK 成功
定时器应用示例
#include <rtthread.h>
/* 定 时 器 的 控 制 块 */
static rt_timer_t timer1;
static rt_timer_t timer2;
static int cnt = 0;
/* 定 时 器 1 超 时 函 数 */
static void timeout1(void *parameter)
{
rt_kprintf("periodic timer is timeout %d\n", cnt);
/* 运 行 第 10 次, 停 止 周 期 定 时 器 */
if (cnt++>= 9)
{
rt_timer_stop(timer1);
rt_kprintf("periodic
timer was stopped! \n");
}
}
/* 定 时 器 2 超 时 函 数 */
static void timeout2(void *parameter)
{
rt_kprintf("one shot timer is timeout\n");
}
int timer_sample(void)
{
/* 创 建 定 时 器 1 周 期 定 时 器 */
timer1 = rt_timer_create("timer1", timeout1,
RT_NULL, 10,
RT_TIMER_FLAG_PERIODIC);
/* 启 动 定 时 器 1 */
if (timer1 != RT_NULL) rt_timer_start(timer1);
/* 创 建 定 时 器 2 单 次 定 时 器 */
timer2 = rt_timer_create("timer2", timeout2,
RT_NULL, 30,
RT_TIMER_FLAG_ONE_SHOT);
/* 启 动 定 时 器 2 */
if (timer2 != RT_NULL) rt_timer_start(timer2);
return 0;
}
/* 导 出 到 msh 命 令 列 表 中 */
MSH_CMD_EXPORT(timer_sample, timer sample);
总结:RT-thread一类的实时操作系统大多提供了时钟管理的功能,里面实现了软件定时器的接口,但是精度有限,若是需要使用高精度定时器,还是使用硬件定时器为好。
Linux操作系统
Jiffies:
内核源代码中几乎到处充斥着 jiffies这样的变量,作为设备驱动程序员对此想必也一定不会陌生,在某些书中它被形象地称为“时钟滴答”。内核源码中针对32位和64位系统分别定义了jiffies和jiffies_64。
通常jffes在Linux系统启动引导阶段被初始化为0,当系统完成了对时钟中断1的初始化之后,在每个时钟中断(“时钟滴答”)处理例程中该值都会被加1。
因此该值储存了系统自最近- -次启动以来的时钟滴答数。在形式.上,它跟我们日常所熟悉的时分秒这样的时间概念有很大的不同,不过对于设备驱动程序而言,出于时间管理的需要,使用jiffies就已经足够,因为它甚少用这种时间形式与应用程序进行沟通。除了时钟中断处理例程中对jffes进行更新外,其他任何模块(驱动程序当然也不例外)都只是读取该值以获得当前时钟计数。
由于jiffies在内核源码中作为一一个全局性变量被导出,所以如果驱动程序中需要读取当前的jffies值,只需在源码中包含头文件linux/jffes.h 即可,比如下面的代码片段:
#include<linux/jffes.h>
unsigned long j , timestamp_ 1, timestamp._2;
j= jffes; //读取当前的时钟计数值
timestamp_ 1 = jiffies + 2 * HZ; //timestamp_ 1为未来的2秒
timestamp_ 2 = jiffies + 3 * HZ /1000; //timnestamp_ 2为未来的3毫秒
Linux设备驱动程序中使用jffies的几个常用的场景分别有时间比较、时间转换以及设置定时器(timer)时对未来时间的设定。
时间比较:
Linux内核提供了一组用以判断时间点先后顺序的宏,这组宏为:
time_ after(a, b) 如果时间点a在时间点b之后,该宏返回true。
time_ _before(a, b) 如果时间点a在时间点b之前,该宏返回true。
time_ after_ _eq(a, b) 该宏类似于time_ after, 但是在a和b两个时间点相等时, 该宏也回 true。
time_ _before_ _eq(a, b) 该宏类似于time_ before, 但是在a和b两个时间点相等 时,该宏也返回true.
time_ in_ range(a, b, c) 该宏用来检查时间点a是否包含在时间间隔[b,c]内,因为 检查包含 边界,所以当a等于b或者c时,该宏也会返回 true。
(在使用以上宏时,参数a和b都应该是unsigned long型变量。)
使用案例:
int demo_ function()
{
unsigned long timeout = jiffles + 2* HZ/1000; //设定超时的时间为2毫秒.
do_ _time_ task0; //调用do_ _time_ _task 来完成某一任务
if (time_ afer(jiffies, timeout)) //根据当前最新的jiffies值来判断是否超时
return task_timeout(); //do_ time_ _task(完成时 间超过了2毫秒,调用超时
//处理函数
return 0;
}
时间转换
有时候,设备驱动程序可能需要将用jiffies表达的时间间隔转化成毫秒ms或者是微秒us的形式,这种情况大多出现在需要将时钟滴答这种形式转化成人类易于理解的ms或者是us这样的时间形式
Linux内核源码为此提供了--组相关的转换函数:
unsigned int jifies_ to _msecs(constunsigned long j);
unsigned int jiffies_ to_ usecs(constunsigned long j);
unsigned long msecs _to_ jiffies(constunsigned int m);
unsigned long usecs_ to_ jiffies(constunsigned int u);
内核同样提供了jiffies变量和timespec、timeval两个数据结构的实例间相互转换的函数:
unsigned long timespec _to_ jiffies(conststruct timespec *value);
void jfies_ to_ timespec(const unsignedlong jffies, struct timespec *value);
unsigned long timeval to_ jiffies(conststruct timeval *value);
void jffies_ to_ _timeval(const unsignedlong jiffies, struct timeval *value);
延时操作:
设备驱动程序中延时操作的常见应用场景是,当CPU通过外部设备的寄存器对设备发出指令时,外设执行相应的动作,在该动作完成之后通过更新比如状态寄存器来告之本次操作的执行结果,CPU需要读取该状态寄存器的值来获得设备的执行结果。因为CPU的速度很快,而外部设备可能需要-定的时间才能完成本次操作,如果CPU在写完寄存器后直接读取设备的状态寄存器,那么很有可能得到错误的结果(设备尚未完成指定的操作,因而还没有更新状态寄存器),所以CPU在对外设发出操作指令后,需要延时一段时间以等待设备操作的完成。这种情形在设备驱动程序中一个简单而典型的代码执行序列应该是:
write_ command reg...); //CPU写外设的寄存器以发起一个操作指令
delay(-.); //延时操作, 等待设备操作完成。它的实现机制是本节要讨论的主题
read_ status_ reg(..); //CPU 读外设的寄存器以获得设备执行结果
从实现延时精度的角度出发,可以将延迟函数分成两大类:一类是基于时钟滴答 jiffes 实现的延迟,因为这类延迟的时间粒度一般在毫秒ms级别,所以被称为“长延时”;另一类的延时精度已经超越了时钟滴答的边界,比如微秒us和纳秒ns级的延迟,显然单纯依靠jiffies已经无法满足要求,此时需要有另外的实现机制,也就是所谓的“短延时”函数。下面先从长延时开始讨论。.
长延时分类:忙等待 ,让出处理器
忙等待:
用忙等待来实现延时是最简单的。虽然因为忙等待的关系其执行效率饱受诟病,但是不得不承认在设备驱动程序早期的开发调试阶段这是-种很简便的手法来判断设备是否能像预期的那样工作。
unsigned longj = jiffies + HZ;
while(time_ before(iffies, j))
cpu_relax();
让出处理器
在忙等待的实现中,处于忙等待中的代码一直占用处理器会导致系统性能降低,于是一种.改进的方案是:当代码进入到while循环时,不再调用cpu_ relax()函 数而是调用schedule()调度函数以让出处理器,这样就解决了忙等待一直浪费处理器的弊端。比如下面的代码:
unsigned longj=jiffies + HZ;
while(time_beforef(iffes, j))
schedule();
这种方法相对于前面的忙等待来说,解决了无端占用CPU的问题,但还不是最佳的解决方案,因为主动调用schedule()函数的进程虽然可以让出处理器,但依然在当前CPU的运行队列中。这使得在空闲的系统中无法进入idle状态,因为即便CPU的运行队列中只有当前-一个进程,它也会陷入让出处理器之后马上又被调度运行,然后再让出处理器这样的怪圈中。无法进入idle状态对系统的电源管理模块来说不是件好事情,因为有些智能化的电源管理模块可以根据当前CPU的负载情况来决定是否改变CPU的运行频率,频率的改变与CPU供电电压的改变是息息相关的。CPU的idle状态可以被电源管理模块所利用,如果后者发现CPU进入了idle状态,可以降低CPU的核心频率从而降低整机的能耗,这在以ARM为主的嵌入式平台上尤为常见。
上面提到的采用schedule()函数的解决方法之所以还不是最佳的,其根本原因在于调用schedule()函数的进程依然处于CPU的运行队列中。为了解决这个问题,此时应该能想到内核提供的另外一-种可供设备驱动程序使用的调度类的基础设施: schedule_ timeout。所以,如果-一个延迟1s的函数可以用下面的这样一个简单的代码段来实现:
delay_ 1s()
{
set_ current _state(TASK_ UNINTERRUPTIBLE);
schedule_ timeoutjiffies + HZ);
}
上面这段代码之所以可以解决直接采用schedule()函 数所带来的负面问题,主要在于在调用schedule_ timeout 之前先调用了set_ current_ state宏将当前进程的状态设置为TASK_ UNINTERRUPTIBLE,这样当随后的schedule_ timeout 函数被调用时,后者的内部实现中调用了schedule()函数,因为当前进程之前的状态已经被设置为TASK_ UNINTERRUPTIBLE,所以在schedule函数中当前进程将会被移出处理器的运行队列,因此也就解决了采用直接调用schedule函数那种方案所带来的不利影响。
事实上,Linux内核还提供了-一个基于上述schedule_ timeout 版本的实现毫秒级睡眠的函数msleep:
void msleep(unsigned int msecs)
{
unsigned long timeout = msecs_ to_jiffies(msecs)+ 1;
while (timeout)
timeout = schedule_ timeout_uninterruptible(timeout);
}
内核中还提供了msleep 的一个变体msleep_interruptible:
unsigned long msleep_interruptible(unsigned int msecs)
{
unsigned long timeout = msecs_ to_jiffies(msecs) + 1;
while (timeout && !signal_pending(current))
timeout = schedule_ timeoutjinterruptible(timeout);
return jiffies_ to_ msecs(timeout);
}
短延时:
有时候设备驱动程序需要更短的延时,比如微秒甚至纳秒级的延迟,这种量级的时延有时候被通俗地称为“短延时”,形象地说明了在延迟粒度上与长延时的区别。基于下面的两个事实,这种量级延迟的实现已经不可能也不必要像前面讨论“长延时”那样直接用时钟滴答jiffies来实现:
Linux内核提供了毫秒、微秒和纳秒级的延迟实现:
void mdelay(unsigned long msecs);
void udelay(unsigned long usecs);
void ndelay(unsigned long nsecs);
这些延迟的实现都是基于忙等待,其中最基础的- -个宏是udelay, 另外两个宏都是通过宏udelay来实现自身功能。
Linux驱动开发中内核定时器的使用
内核定时器是设备驱动程序中经常要用到的另一个重要的内核设施。如果驱动程序希望在将来某个可度量的时间点到期后,由内核安排执行某项任务(此处的任务通常是驱动程序自身定义的某个函数,接下来的叙述中称之为定时器函数),便可以使用定时器来完成。
驱动程序等内核模块如果要使用定时器,首先应该定义一个定时器类型的变量。struct timer_ list 是内核提供的一个用来表示定时器的数据结构,其定义如下(删去了一些用于调试及统计信息的成员):
struct timer_ list {
struct list_ head entry;
unsigned long expires; //指定定时器的到期时间。
struct tvec_ base * base;
void (*function)(unsigned long); /*定时器函数。当expires中指定的时间到 期时,该函数将被触发。*/
unsigned long data; /*定时器对象中携带的数据。通常的用途是,当定时器函数 被调用时,内核将把该成员作为实际参数传递给定时器函 数。之所以要这样做,是因为定时器函数将在中断上下文 中执行,而非当前进程的地址空间中。*/
int slack;
};
使用示例代码:
struct device_regs *devreg = NULL;//定义一个用于表示设备寄存器的结构体指针
struct timer_list demo_timer; //定义一个内核定时器对象
/*定义定时器函数,当定时器对象demo_timer 中expires 成员指定的时间到期后,该函数将被调用*/
static void demo_timer_func (unsignedlong data)
{
//在定时器函数中重新启动定时器以实现轮询的目的
demo_timer.expires = jiffies + HZ;
add_timer(&demo_timer);
//定时器函数将data参数通过类型转换获得设备寄存器的结构体指针
struct device_regs *preg = (struct device_regs *) data;
//定时器函数此后将会读取设备状态
...
}
//用于打开设备的函数实现
static int demo_dev_open(..)
{
//分配设备寄存器结构体的指针变量,最好放在模块初始化函数中...
devreg = kmalloc(sizeof(struct device_regs), GFP_KERNEL);
init_ timer(&demo_ timer); //调用内核函数init timer来初始化定时器对象demo //timer
demo timer.expires = jiffies + HZ; //设定定时器到期时间点,从现在开始的1秒钟
demo_timer.data = (unsigned long) devreg;//将设备寄存器指针地址作为参数
demo_timer.function = &demo_ timer_func;
add_timer(&demo_timer);
}
//用于关闭设备的函数实现
static int demo_ dev_release(..)
{
...
del_timer_sync(&demo_timer); //删除定时器对象
}
init_timer
该函数主要初始化定时器对象中与内核实现相关的成员,所以设备驱动程序在开始使用定时器对象前,应该调用init timer, 这样从内核层面出发,后续对定时器的一些操作才会被内核所支持,下面在讨论add_ timer 函数时会看到这一点。
add_timer
当程序定义了一个定时器对象,并且通过init_timer函数及相应代码对该定时器对象中的expires、data 和function等成员初始化之后,程序需要调用add_ timer 将该定时器对象加入到系统中,这样定时器才会在expires 表示的时间点到期后被触发。可以想见,add_timer函数的内部实现将不再独立,它必然会和内核中关于定时器的基础架构发生关联。
del_timer 和del_timer_sync
同add_timer函数相反,del_timer类的函数负责从系统的定时器管理队列中摘除-一个定时器对象。del_timer 和del_timer_sync 的函数原型为:
int del_timer(struct timer_list *timer);
int del_timer_sync(struct timer_list*timer);
小结:
定时器在设备驱动程序中最常见也最典型的使用场景是用来实现轮询机制,内核为定时器机制设计了一套完整的机制,对于设备驱动程序而言,只需定义一个定时器对象并指定其到期时间及实现一个定时器函数,然后通过add_timer 或者mod_timer 将该定时器对象加到系统中即可。当一个定时器对象到期被执行时,内核会将其从系统的定时器管理队列中摘除下来,所以为了实现轮询,驱动程序需要在定时器函数中重新将该定时器对象加入到管理队列中。因为定时器的实现机制是基于系统中的硬件时钟中断,因为受硬件时钟精度以及时钟中断softirq固有的实现特性,定时器的精度并非完美,但是对于绝大多数的设备驱动而言已经足够了。