时间延迟
如何度量时间差,比较时间
如何获得当前时间
如何将操作延迟指定的一段时间
如何调度异步函数到指定的时间之后执行
度量时间差
内核通过定时器中断来跟踪时间流。
时钟中断由系统定时硬件以周期性的间隔产生,这个间隔由内核根据HZ的值设定,HZ是一个与体系结构有关的常数。
每当时钟中断发生时,内核内部计数器的值就增加一。
这个计数器的值在系统引导时被初始化为0,它的值就是自上次操作系统引导以来的时钟滴答数。
驱动程序开发者通常访问的是jiffies变量。
比较缓存值和当前值时,应该使用下面的宏:
#include<linux/jiffies.h>
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_before_eq(unsigned long a, unsigned long b);
用户空间的时间表述方法(使用struct timeval和struct timespec)
内核提供以下辅助函数用于完成jiffies值和这些结构间的转换:
#include<linux/time.h>
unsigned long timespec_to_jiffies(struct timespec *value);
void jiffies_to_timespec(unsigned long jiffies, struct timespec *value);
unsigned long timeval_to_jiffies(struct timeval *value);
void jiffies_to_timeval(unsigned long jiffies, struct timeval *value);
#include<linux/jiffies.h>
u64 get_jiffies_64(void);
实际的时钟频率对用户空间来讲几乎是完全不可见的。
当用户空间程序包含param.h时,HZ宏始终被扩展为100.
对用户来讲,如果想知道定时器中断的HZ值,只能通过/proc/interrupts获得。
例如,将通过/proc/interrupts获得的计数值除以/proc/uptime文件报告的系统运行时间,即可获得内核的确切HZ值。
最有名的计数器寄存器是TSC(timestamp counter,时间戳计数器),它是一个64位的寄存器,记录CPU时钟周期数,从内核空间和用户空间都可以读取它。
获取当前时间
内核一般通过jiffies值来获取当前时间,该数值表示的是自最近一次系统启动到当前的时间间隔,
它的生命期只限于系统的运行期(uptime)。
驱动程序可以利用jiffies的当前值来计算不同事件间的时间间隔。
对真实世界的时间处理通常最好留给用户空间,C函数库为我们提供了更好的支持。
内核提供了将墙钟时间转换为jiffies值的函数:
#include <linux/time.h>
unsigned long mktime(unsigned int year, unsigned int mon, unsigned int day, unsigned int hour, unsigned int min, unsigned int sec);
直接处理墙钟时间意味着正在实现某种策略。
<linux/time.h>导出了do_gettimeofday函数,该函数用秒或微妙值来填充一个指向struct timeval的指针变量。
gettimeofday系统调用中用的是同一变量。
do_gettimeofday的原型:
#include <linux/time.h>
void do_gettimeofday(struct timeval *tv);
当前时间也可以通过xtime变量(类型为struct timespec)来获得,但精度要差一些。
内核提供一个辅助函数current_kernel_time:
#include <linux/time.h>
struct timespec current_kernel_time(void);
current_kernel_time以纳秒精度表示,但只有时钟滴答的分辨率;
do_gettimeofday持续报告靠后的时间,但总不会晚于下一个定时器滴答。
延迟执行
在不需要CPU时主动释放CPU,这可以通过调用schedule函数实现,
超时
实现延迟最好的方法应该是让内核为我们完成相应工作。
存在两种构造基于jiffies超时的途径,使用哪个依赖于驱动程序是否在等待其他事件。
如果驱动程序使用等待队列来等待其他一些事情,而我们同时希望在特定时间段中运行,则可以使用wait_event_timeout或者wait_event_interruptible_timeout函数:
#include <linux/wait.h>
long wait_event_timeout(wait_queue_head_t q, conditon, long timeout);
long wait_event_interruptible_timeout(wait_queue_head_t q, conditon, long timeout);
上述函数会在给定的等待队列上休眠,但是会在超时(用jiffies表示)到期时返回。
这里的timeout值表示的是要等待的jiffies值。
在某个硬件驱动程序中使用wait_event_timeout和wait_event_interruptible_timout时,执行的继续可通过下面两种方式获得:
其他人在等待队列上调用wake_up,或则会超时到期。
为了适应这种特殊情况(不等待特定事件而延迟),内核提供了schedule_timeout函数,这样可避免声明和使用多余的等待队列头:
#include <linux/sched.h>
signed long schedule_timeout(signed long timeout);
timeout是用jiffies表示的延迟时间,正常的返回值是0,除非在给定超时值到期前函数返回(比如响应某个信号)。
schedult_timeout要求调用者首先设置当前进程的状态。
典型的调用代码如下所示:
set_current_state(TASK_INTERRUPTIBLE);
schedule_timeout(delay);
wait_event_interruptible_timeout在内部依赖于schedule_timeout函数。
调度器只会在超时到期且其状态变成TASK_RUNNING时才会运行这个进程。
如果要实现不可中断的延迟,可使用TASK_UNINTERRUPTIBLE。
如果忘记改变当前进程的状态,则对schedule_timeout的调用和对schedule的调用一样,内核为我们构造的定时器就不会真正起作用。
短延迟
ndelay、udelay和mdelay这几个内核函数可很好完成短延迟任务,它们分别延迟指定数量的纳秒、微妙和毫秒时间。
#include <linux/delay.h>
void ndelay(unsigned long nsecs);
void udelay(unsigned long nsecs);
void mdelay(unsigned long nsecs);
这些函数的实现包含在<asm/delay.h>中,其实现和具体的体系架构相关。
这三个延迟函数均是忙等待函数。
内核定时器
可用来在未来的某个特定时间点(基于时钟滴答)调度执行某个函数,从而可用于完成许多任务。
内核本身也在许多情况下使用了定时器,包括在schedule_timeout的实现中。
一个内核定时器是一个数据结构,它告诉内核在用户定义的时间点使用用户定义的参数来执行一个用户定义的函数。其实现位于<linux/timer.h>和kerneltimer.c文件。
被调度运行的函数几乎肯定不会在注册这些函数的进程正在执行时运行。这些函数会异步地运行。
内核定时器常常是作为“软件中断”的结果而运行的。
如果处于进程上下文之外(比如在中断上下文中),则必须遵守如下规则:
不允许访问用户空间;
current指针没有任何意义;
不能执行休眠或调度,原子代码不可以调用schedule或者wait_event,也不能调用任何可能引起休眠的函数。(kmalloc(..., GFP_KERNEL),信号量)
函数in_interrupt()可用来判断是否运行于中断上下文。如果是就返回非零值,无论是硬件中断还是软件中断。
函数in_atomic(),当调度不被允许时,返回值也是非零值。
调度不被允许的情况包括硬件和软件中断上下文以及拥有自旋锁的任何时间点。
内核定时器的另一个重要特性是,任务可以将自己注册以后在稍后的时间重新运行。这种可能性是因为每个timer_list结构都会在运行之前从活动定时器链表中移走,这样就可以链入其他的链表。
在SMP系统中,定时器函数会由注册它的同一CPU执行,这样可以尽可能获得缓存的局域性(locality)。一个注册自己的定时器时钟会在同一CPU上运行。
定时器也会是竞态的潜在来源,任何通过定时器函数访问的数据结构都应该针对并发访问进行访问。
定时API
内核为驱动程序提供了一组用来声明、注册和删除内核定时器的函数。
#include <linux/timer.h>
struct timer_list{
unsigned long expires;
void (*function)(unsigned long);
unsigned long data;
};
void init_timer(struct timer_list *timer);
struct timer_list TIMER_INITIALIZER(_function, _expires, _data);
void add_timer(struct timer_list *timer);
int del_timer(struct timer_list *timer);
参数expires字段表示期望定时器执行的jiffies值,到达该jiffies值时,将调用function函数,并传递data作为参数。
如果需要通过这个参数传递多个数据项,那么可以将这些数据项捆绑成一个数据结构,然后将该数据结构的指针强制转换成unsigned long传入。
内核定时器的实现
不管何时内核代码注册了一个定时器,其操作最终会由internal_add_timer(定义在kernel/timer.c)执行,该函数又会将新的定时器添加到和当前CPU关联的“级联表”中的定时器双向链表中。
级联表的工作方式如下:如果定时器在接下来的0~255个jiffies中到期,则该定时器就会被添加到256个链表中的一个,这些链表专用于短期定时器。
当__run_timers被激发时,它会执行当前定时器滴答上的所有挂起的定时器。如果jiffies当前是256的倍数,该函数还会将下一级定时器链表重新散列到256个短期链表中,同时还可能根据上面jiffies的位划分对将其他级别的定时器做级联处理。
函数__run_timers运行在原子上下文中。
定时器会在正确的时间到期,即使运行的不是抢占式的内核,而CPU会忙于内核空间。
尽管系统似乎被忙等待系统调用整个锁住,但内核定时器仍然可很好地工作。
但内核定时器会受到jitter以及由硬件中断、其他定时器和异步任务所产生的影响。
所以不适合于工业环境下的生产系统,对这类任务,需要借助某种实时的内核扩展。
tasklet
(小任务机制)
中断管理中大量使用了这种机制。
和内核定时器的
相同点:
时钟在中断期间运行,始终会在调度它们的同一CPU上运行,而且都接收一个unsigned long参数。
也会在“软件中断”上下文以原子模式执行。
不同点:
不能要求tasklet在某个给定时间执行。
调度一个tasklet,表明我们只是希望内核选择某个其后的时间来执行给定的函数。
中断处理例程必须尽可能快地管理硬件中断,而大部分数据管理则可以安全地延迟到其后的时间。
软件中断是打开硬件中断的同时执行某些异步任务的一种内核机制。
tasklet以数据结构的形式存在,并在使用前必须初始化。
调用特定的函数或者使用特定的宏来声明该结构,即可完成tasklet的初始化:
#include <linux/interrupt.h>
struct tasklet_struct{
void(*func)(unsigned long);
unsigned long data;
};
void tasklet_init(struct tasklet_struct *t, void(*func)(unsigned long), unsigned long data);
DECLARE_TASKLET(name, func, data);
DECLARE_TASKLET_DISABLED(name, func, data);
tasklet的特性:
一个tasklet可在稍后被禁止或者重新启动,只有启用的次数和禁止的次数相同时,tasklet才会被执行;
和定时器类似,tasklet可以注册自己本身;
tasklet可被调度以在通常的优先级或者高优先级执行,高优先级的tasklet总会首先执行;
如果系统负荷不重,则tasklet会立即得到执行,但始终不会晚于下一个定时器滴答;
一个tasklet可以和其他tasklet并发,但对自身来讲是严格串行处理的,也就是说同一tasklet永远不会在多个处理器上同时运行;
tasklet始终会在调度自己的同一CPU上运行。
内核为每个CPU提供了一组ksoftirq内核线程,用于运行“软件中断”处理例程,比如tasklet_action函数。
tasklet相关的内核接口:
/*这个函数禁用指定的tasklet;
该tasklet仍然可以用tasklet_schedule调度,但其执行被推迟,直到该tasklet被重新启用;
如果tasklet当前正在运行,该函数会进入忙等待直到tasklet退出为之;
在调用tasklet_disable之后,可以确信该tasklet不会在系统中的任何地方运行。*/
void tasklet_disable(struct tasklet_struct *t);
/*禁止指定的tasklet,但不会等待任何正在运行的tasklet退出。
该函数返回后,tasklet是禁用的,而且在重新启用之前,不会再次被调度。
当该函数返回时,指定的tasklet可能仍在其他CPU上执行。*/
void tasklet_disable_nosync(struct tasklet_struct *t);
/*启用一个先前被禁用的tasklet。
如果该tasklet已经被调度,它很快就会运行;
对tasklet_enable的调用必须和每个对tasklet_disable的调用匹配;
内核对每个tasklet保存有一个“禁用计数”。*/
void tasklet_enable(struct tasklet_struct *t);
/*调度执行指定的tasklet。
如果在获得运行机会之前,某个tasklet被再次调度,则该tasklet只会运行一次。
如果该tasklet运行时被调度,就会在完成后再次运行。这样可确保正在处理事件时发生的其他事件也会被接收并注意到,这种行为也允许tasklet重新调度自身。*/
void tasklet_schedule(struct tasklet_struct *t);
/*调度指定的tasklet以高优先级执行。
当软件中断处理例程运行时,它会在处理其他软件中断任务之前处理高优先级的tasklet。*/
void tasklet_hi_schedule(struct tasklet_struct *t);
/*该函数确保指定的tasklet不会被再次调度运行;
当设备要被关闭或者模块要被移除时调用;
如果tasklet正被调度执行,该函数会等待其退出;
如果tasklet重新调度自己,则应该避免在调用tasklet_kill之前完成重新调度,这和del_timer_sync的处理类似。*/
void tasklet_disable(struct tasklet_struct *t);
tasklet的实现在kernel/softirq.c中。
其中有两个(通常优先级和高优先级)tasklet链表,它们作为per-CPU数据结构而声明,并且使用了类似内核定时器那样的CPU相关机制。
工作队列
工作队列(workqueue)类似于tasklet,它们都允许内核代码请求某个函数在将来的时间被调用。
区别:
tasklet在软件中断上下文中运行,所有的tasklet代码必须是原子的。工作队列函数在一个特殊内核进程的上下问文中运行,工作队列函数可以休眠。
tasklet始终运行在被初始提交的同一处理器上,但这只是工作队列的默认方式。
内核代码可以请求工作队列函数的执行延迟给定的时间间隔。
两者的关键区别在于:tasklet会在很短的时间段内很快执行,并且以原子模式执行,而工作队列函数可具有更长的延迟并且不必原子化。
工作队列有struct workqueue_struct类型,该结构定义在<linux/workqueue.h>中。
在使用之前,必须显式地创建一个工作队列:
struct workqueue_struct *create_workqueue(const char *name);
struct workqueue_struct *create_singlethread_workqueue(const char *name);
每个工作队列有一个或多个专用的进程(“内核线程”),这些进程运行提交到该队列的函数。
如果使用create_workqueue,则内核会在系统中的每个处理器上为该工作队列创建专用的线程。
要向一个工作队列提交一个任务,需要填充一个work_struct结构,这可通过下面的宏在编译时完成:
DECLARE_WORK(name, void(*function)(void *), void *data);
如果要在运行时构造work_struct结构,可使用下面两个宏:
INIT_WORK(struct work_struct *work, void(*function)(void *), void *data);
PREPARE_WORK(struct work_struct *work, void(*function)(void *), void *data);
如果要将工作提交到工作队列,则可使用:
int queue_work(struct workqueue_struct *queue, struct work_struct *work);
int queue_delayed_work(struct workqueue_struct *queue, struct work_struct *work, unsigned long delay);
如果被成功添加到队列,则返回值为1.返回值为非零时意味着给定的work_struct结构已经等待在该队列中。
在将来的某个时间,工作函数会被调用,并传入给定的data值。
该函数不能访问用户空间,这是因为它运行在内核线程,而该线程没有对应的用户空间可以访问。
如果要取消某个挂起的工作队列入口项,可调用:
int cancel_delayed_work(struct work_struct *work);
为了绝对确保在cancel_delayed_work返回0之后,工作函数不会在系统中的任何地方运行,则应该随后调用下面的函数:
void flush_workqueue(struct workqueue_struct *queue);
函数返回后,任何在该调用之前被提交的工作函数都不会在系统任何地方运行。
在结束对工作队列的使用后,可调用下面的函数释放相关资源:
void destroy_workqueue(struct work_queue_struct *queue);
共享队列
设备驱动程序可以使用内核提供的共享的默认工作队列。
初始化work_struct结构
static struct work_struct jiq_work;
INIT_WORK(&jiq_work, jiq_print_wq, &jiq_data);
int schedule_work(struct work_struct *work);
如果用户读取延迟的设备,工作函数会将自己以延迟模式重新提交到工作队列,这时使用schedule_delayed_work函数:
int schedule_delayed_work(struct work_struct *work, unsigned long delay);
如果需要取消已提交到共享队列中的工作入口项,则可使用cancel_delayed_work函数。但是,刷新共享工作队列时需要另一个函数:
void flush_scheduled_work(void);