这一部分主要是对于内核代码如何处理时间问题的解析,主要学习的内容:
- 如何度量时间差,如何比较时间
- 如何获取当前时间
- 如何将操作延迟到指定的一段时间
- 如何调度异步函数到指定的时间之后去执行
度量时间差
内核通过定时器中断来跟踪时间流。下面是一些相关的概念:
- 时钟中断:由系统定时硬件以周期性的间隔产生。
- hz:上述间隔由hz的值设定,hz是一个与体系结构相关的常数,定义在<linux/param.h>中,大多数平台的hz值定义的默认范围:50-1200,软件仿真器:24,x86PC:1000
- 计数器:发生时钟中断一次,计数器加一,这个计数器的值(只有)在系统引导时被初始化为0。
- Jiffies_64变量 :计数器被看为的一个64位的变量(即使在32位架构上也是64位)
- jiffies变量:Jiffies_64变量基本不适用,一般用jiffies变量,unsigned long 型变量,要么与jiffies_64相同,要么取其低32位。
使用jiffies计数器
读计数器和读取计数器的工具函数都包含在<linux/jiffies.h>中,但是一般都使用<linux/sched.h>,后者会自动添加jiffies.h。注意:jiffies和jiffies_64都是只读变量。
使用的简单例子:
#include<linux/jiffies.h>
unsigned long j,stamp_1,stamp_half,stamp_n;
j=jiffies; //读取当前值
stamp_1=j+HZ; //未来的一秒
stamp_half=j+HZ/2; //未来的0.5秒
stamp_n=j+n*HZ/1000; // 未来的n毫秒
而比较缓存值(即上面的stamp等值)和当前值时,要使用以下定义的宏:
#include<linux/jiffies.h>
int time_after(unsigned long a,unsigned long b); //a比b后,返回真
int time_before(unsigned long a,unsigned long b); //a比b前,返回真
int time_after_eq(unsigned long a,unsigned long b); //a比b后或相等,返回真
int time _before_eq(unsigned long a,unsigned long b); //a比b前或相等,返回真
上述几个宏会将计数器值转换为signed long,相减,然后比较结果。
如果需要以安全的方式计算两个jiffies实例之间的差,如下:
diff = (long) t2 - (long) t1;
而通过下面的方法,可将两个jiffies的差转换为毫秒值:
msec = diff *1000/HZ;
用户空间和内核空间的时间表述方法的转换:
- 用户空间方法:timeval,timespec
- 内核空间方法:jiffies
#include<linux/time.h>
struct timespec {
time_t tv_sec; /* 秒 */
long tv_nsec; /*纳秒 */
};
struct timeval {
time_t tv_sec; /* 秒 */
SUSEconds_t tv_usec; /*纳秒 */
};
unsigned long timespec_to_jiffies(struct timespec *value); //time_spec转jiffies
void jiffies_to_timespec(unsigned long jiffies,struct timespec *value);
unsigned long timeval_to_jiffies(struct timeval *value); //timeeval转jiffies
void jiffies_to _timeval(unsigned long jiffies,struct timeval *value);
处理器特定的寄存器
如果需要精度很高的计时,jiffies已不可满足需要,这时就引入了一种技术就是CPU包含一个随时钟周期不断递增的计数寄存器。这是完成高分辨率计时任务的唯一可靠途径。不管该寄存器是否置0,我们都强烈建议不要重置它。
最有名的计数器 TSC:这是一个64位寄存器,记录CPU的时钟周期数,从内核空间和用户空间都可以读取它。
获取当前时间
内核一般通过jiffies值来获取当前时间,该数值表示自最近一次系统启动到当前时间的间隔,但它和驱动设备程序无关,因为它的生命周期只限于系统的运行期(uptime)。但驱动程序可以利用jiffies的当前值来计算不同事件间的时间间隔(比如输入设备驱动程序就用它来分辨鼠标的单双击)。
内核中将时间转化成jiffies值的函数:
#include<linux/time.h>
unsigned long mktime(unsigned int year,unsigned int month,
unsigned int day, unsigned int hour,
unsigned int minute,unsigned int second);
获取当前时间的两种方式:
- 为了处理绝对时间, <linux/time.h> 导出了 do_gettimeofday 函数,它填充一个指向 struct timeval 的指针变量:
#include<linux/time.h>
void do_gettimeofday(structtimeval*tv);
- 当前时间还可以通过xtime变量(类型为struct timespec)来获得,但精度比上面差一些。
全局变量xtime:它是一个timeval结构类型的变量,用来表示当前时间距UNIX时间基准1970-01-01 00:00:00的相对秒数值。
内核提供给了一个辅助函数:
struct timespec current_kernel_time(void); /*上述方法得到的数据都表示当前时间距UNIX时间基准1970-01-01 00:00:00的相对时间*/
延迟执行
驱动程序经常需要将某些特定代码延迟一段时间后执行,一般是为了让硬件能完成某些任务。而实现延迟的方式主要有:
长延迟
长延迟主要适用于驱动程序需要延迟比较长的时间,长于一个时间滴答。而实现长延迟的技术主要有:
- 忙等待
最简单的实现方法(不是很推荐),主要是通过监视一个jiffies计数器的循环,实现代码:
while(time_before(jiffies,j1)) //j1表示延迟终止时的jiffies值
cpu_relax(); //在许多系统上,这个函数不做任何事,但在多线程系统上,他可能将处理器让给其它线程。
- 让出处理器
因为忙等待给系统整体增加了沉重的负担,因此需要更好的技术:在不需要CPU的时候主动释放CPU。这个可以通过调用schedule函数实现,该函数在<linux/sched.h>中申明:
while(time_before(jiffies,j1)) //j1表示延迟终止时的jiffies值
schedule();
- 超时
实现延迟的最好方法应该是让内核为我们完成相应的工作。这有两种构造jiffies超时的实现途径,具体使用哪个要看具体情况:
若驱动使用一个等待队列来等待某些其他事件,并想确保它在一个特定时间段内运行,可使用:
#include<linux/wait.h>
long wait_event_timeout(wait_queue_head_t q, condition,long timeout);
long wait_event_interruptible_timeout(wait_queue_head_t q, condition,long timeout);
/*这些函数在给定队列上睡眠, 但是它们在超时(以 jiffies 表示)到后返回。如果超时,函数返回 0; 如果这个进程被其他事件唤醒,则返回以 jiffies 表示的剩余的延迟实现;返回值从不会是负值*/
为了实现进程在超时到期时被唤醒而又不等待特定事件(避免声明和使用一个多余的等待队列头),内核提供了 schedule_timeout 函数:
#include<linux/sched.h>
signed long schedule_timeout(signedlong timeout);
/*timeout 是要延时的 jiffies 数。除非这个函数在给定的 timeout 流失前返回,否则返回值是 0 。
schedule_timeout 要求调用者首先设置当前的进程状态。为获得一个不可中断的延迟, 可使用 TASK_UNINTERRUPTIBLE 代替。
如果你忘记改变当前进程的状态, 调用 schedule_time 如同调用 shcedule,建立一个不用的定时器。一个典型调用如下:*/
set_current_state(TASK_INTERRUPTIBLE);
schedule_timeout (delay);
短延迟
当一个设备驱动需要处理硬件的延迟(latency潜伏期), 涉及到的延时通常最多几个毫秒,在这个情况下, 不应依靠时钟嘀哒,而是内核函数 ndelay, udelay和 mdelay ,他们分别延后执行指定的纳秒数, 微秒数或者毫秒数,定义在 <asm/delay.h>,原型如下:
#include<linux/delay.h>
void ndelay(unsignedlong nsecs);
void udelay(unsignedlong usecs);
void mdelay(unsignedlong msecs);
注意:
- 这 3 个延时函数是忙等待,其他任务在时间流失时不能运行。
- 为避免在循环计算中整数溢出, 传递给udelay 和 ndelay的值有一个上限,如果你的模块无法加载和显示一个未解决的符号:__bad_udelay, 这意味着你调用 udleay时使用太大的参数。
通用的规则:若试图延时几千纳秒, 应使用 udelay 而不是 ndelay; 类似地, 毫秒规模的延时应当使用 mdelay 完成而不是一个更细粒度的函数。
有另一个方法获得毫秒(和更长)延时而不用涉及到忙等待的方法是使用以下函数(在<linux/delay.h> 中声明):
void msleep(unsignedint millisecs);
unsigned long msleep_interruptible(unsignedint millisecs);
void ssleep(unsignedint seconds)
若能够容忍比请求的更长的延时,应使用 schedule_timeout, msleep 或 ssleep。
内核定时器
当需要调度一个以后发生的动作, 而在到达该时间点时不阻塞当前进程, 则可使用内核定时器。内核定时器用来调度一个函数在将来一个特定的时间(基于时钟嘀哒)执行,从而可完成各类任务。
内核定时器是一个数据结构, 它告诉内核在一个用户定义的时间点使用用户定义的参数执行一个用户定义的函数,函数位于 <linux/timer.h> 和 kernel/timer.c 。被调度运行的函数几乎确定不会在注册它们的进程在运行时运行,而是异步运行。实际上, 内核定时器通常被作为一个"软件中断"的结果而实现。当在进程上下文之外(即在中断上下文)中运行程序时, 必须遵守下列规则:
- 不允许访问用户空间;
- current 指针在原子态没有意义;
- 不能进行睡眠或者调度. 例如:调用 kmalloc(…, GFP_KERNEL) 是非法的,信号量也不能使用因为它们可能睡眠。
- 内核代码可以通过调用函数 in_interrupt()能够告知是否它在中断上下文中运行,它无需参数,并如果处理器运行在中断上下文就返回非零。
- 通过调用函数 in_atomic()能够告知调度是否被禁止,若调度被禁止返回非零; 调度被禁止的情况包含硬件和软件中断上下文以及任何持有自旋锁的的任何时间。
在后一种情况, current 可能是有效的,但是访问用户空间是被禁止的,因为它能导致调度发生. 当使用 in_interrupt()时,都应考虑是否真正该使用的是 in_atomic 。他们都在 <asm/hardirq.h> 中声明。
内核定时器的另一个重要特性是任务可以注册它本身在后面时间重新运行,因为每个 timer_list 结构都会在运行前从激活的定时器链表中去连接,因此能够立即链入其他的链表。一个重新注册它自己的定时器一直运行在同一个 CPU.
即便在一个单处理器系统,定时器是一个潜在的态源,这是异步运行直接结果。因此任何被定时器函数访问的数据结构应当通过原子类型或自旋锁被保护,避免并发访问。
定时器 API
内核提供给驱动许多函数来声明、注册以及删除内核定时器:
#include <linux/timer.h>
struct timer_list {
struct list_head entry;
unsigned long expires; /*期望定时器运行的绝对 jiffies 值,不是一个 jiffies_64 值,因为定时器不被期望在将来很久到时*/
void (*function)(unsigned long); /*期望调用的函数*/
unsigned long data; /*传递给函数的参数,若需要在参数中传递多个数据项,可以将它们捆绑成单个数据结构并且将它的指针强制转换为 unsiged long 的指针传入。这种做法在所有支持的体系上都是安全的并且在内存管理中相当普遍*/
struct tvec_t_base_s *base;
#ifdef CONFIG_TIMER_STATS
void *start_site;
char start_comm[16];
int start_pid;
#endif
};
/*这个结构必须在使用前初始化,以保证所有的成员被正确建立(包括那些对调用者不透明的初始化):*/
void init_timer(struct timer_list *timer);
/*在初始化后和调用 add_timer 前,可以改变 3 个公共成员:expires、function和data*/
struct timer_list TIMER_INITIALIZER(_function, _expires, _data);
void add_timer(struct timer_list * timer); //注册定时器在当前cpu运行
int del_timer(struct timer_list * timer); /*在到时前禁止一个已注册的定时器*/
int del_timer_sync(struct timer_list *timer); /*如同 del_timer ,但还保证当它返回时, 定时器函数不在任何 CPU 上运行,以避免在 SMP 系统上竞态, 并且在 单处理器内核中和 del_timer 相同。这个函数应当在大部分情况下优先考虑。 如果它被从非原子上下文调用, 这个函数可能睡眠,但是在其他情况下会忙等待。当持有锁时要小心调用 del_timer_sync ,如果这个定时器函数试图获得同一个锁, 系统会死锁。如果定时器函数重新注册自己, 调用者必须首先确保这个重新注册不会发生; 这通常通过设置一个" 关闭 "标志来实现, 这个标志被定时器函数检查*/
int mod_timer(struct timer_list *timer, unsigned long expires); /*更新一个定时器的超时时间, 常用于超时定时器。也可在正常使用 add_timer时在不活动的定时器上调用mod_timer*/
int timer_pending(const struct timer_list * timer); /*宏定义 返回一个布尔值说明是否这个定时器结构已经被注册*/
void del_timer(struct timer_list * timer);
void del_timer_sync(struct timer_list * timer);
/*从激活的定时器链表中去除一个定时器. 后者保证这定时器当前没有在另一个 CPU
上运行*/
内核定时器实现需要满足的需求和假定:
- 定时器管理必须尽可能简化.
- 设计应当随着激活的定时器数目上升而很好地适应.
- 大部分定时器在几秒或最多几分钟内到时, 而带有长延时的定时器是相当少见.
- 一个定时器应当在注册它的同一个 CPU 上运行
tasklet和工作队列
tasklet
和定时问题相关的另一个内核设施时tasklet(小任务)机制,中断管理中大量使用了这种机制。
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);
#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 }
tasklet相关的内核接口,可在tasklet初始化后使用:
void tasklet_disable(struct tasklet_struct *t);
/*函数暂时禁止给定的 tasklet被 tasklet_schedule 调度,直到这个 tasklet 被再次被enable;
若这个 tasklet 当前在运行, 这个函数忙等待直到这个tasklet退出*/
void tasklet_disable_nosync(struct tasklet_struct *t);
/*和tasklet_disable类似,但是tasklet可能仍然运行在另一个 CPU */
void tasklet_enable(struct tasklet_struct *t);
/*使能一个之前被disable的 tasklet;若这个tasklet已经被调度, 它会很快运行。
tasklet_enable 和tasklet_disable必须匹配调用, 因为内核跟踪每个 tasklet 的"禁止次数"*/
void tasklet_schedule(struct tasklet_struct *t);
/*调度 tasklet 执行,如果tasklet在运行中被调度, 它在完成后会再次运行;
这保证了在其他事件被处理当中发生的事件受到应有的注意. 这个做法也允许一个 tasklet 重新调度它自己*/
void tasklet_hi_schedule(struct tasklet_struct *t);
/*和tasklet_schedule类似,只是在更高优先级执行。
当软中断处理运行时, 它处理高优先级 tasklet 在其他软中断之前,只有具有低响应周期要求的驱动才应使用这个函数, 可避免其他软件中断处理引入的附加周期*/
void tasklet_kill(struct tasklet_struct *t);
/*确保了 tasklet 不会被再次调度来运行,通常当一个设备正被关闭或者模块卸载时被调用。
如果 tasklet 正在运行, 这个函数等待直到它执行完毕。
若 tasklet 重新调度它自己,则必须阻止在调用 tasklet_kill 前它重新调度它自己,如同使用 del_timer_sync*/
tasklet特性:
- 一个tasklet可在稍后被禁止或者重新启用;只有启用的次数和禁止的次数相同时,tasklet才会被执行。
- 和定时器类似,tasklet可以自己注册自己。
- tasklet可被调度以在通常的优先级或者高优先级执行。高优先级的tasklet总会优先执行。
- 如果系统负荷不重,则tasklet会立即执行,但始终不会晚于下一个定时器滴答
- 一个tasklet可以和其它tasklet并发,但对自身来讲是严格串行处理的,也就是说,同一tasklet永远不会在多个处理器上同时运行:tasklet始终会调度自己在同一CPU上运行;
工作队列
表面来看,工作队列类似于tasklet:允许内核代码请求某个函数在将来的时间被调用。
但其实还是有很多不同:
- 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); /*单个工作线程*/
提交一个任务给一个工作队列, 你需要填充一个 work_struct 结构(书上的方法在后续的内核中不适用):
/*需要填充work_struct或delayed_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;
};
struct delayed_work { //定义delayed_work结构
struct work_struct work;
struct timer_list timer;
};
DECLARE_WORK(n, f)
/*n 是声明的work_struct结构名称, f是要从工作队列被调用的函数*/
DECLARE_DELAYED_WORK(n, f)
/*n是声明的delayed_work结构名称, f是要从工作队列被调用的函数*/
/*若在运行时需要建立 work_struct 或 delayed_work结构, 使用下面 2 个宏定义:*/
INIT_WORK(struct work_struct *work, void (*function)(void *));
PREPARE_WORK(struct work_struct *work, void (*function)(void *));
INIT_DELAYED_WORK(struct delayed_work *work, void (*function)(void *));
PREPARE_DELAYED_WORK(struct delayed_work *work, void (*function)(void *));
/* INIT_WORK 做更加全面的初始化结构的工作,在第一次建立结构时使用. PREPARE_WORK做几乎同样的工作, 但是它不初始化用来连接 work_struct或delayed_work 结构到工作队列的指针。如果这个结构已经被提交给一个工作队列, 且只需要修改该结构,则使用 PREPARE_WORK 而不是 INIT_WORK*/
/*有 2 个函数来提交工作给一个工作队列:*/
int queue_work(struct workqueue_struct *queue, struct work_struct *work);
int queue_delayed_work(struct workqueue_struct *queue, struct delayed_work *work, unsigned long delay);
/*每个都添加work到给定的workqueue。如果使用 queue_delay_work, 则实际的工作至少要经过指定的 jiffies 才会被执行。 这些函数若返回 1 则工作被成功加入到队列; 若为0,则意味着这个 work 已经在队列中等待,不能再次加入*/
取消一个挂起的工作队列入口项可以调用:
int cancel_delayed_work(struct delayed_work *work);
void cancel_work_sync(struct work_struct *work);
当用完一个工作队列,可以去掉它,使用:
void destroy_workqueue(struct workqueue_struct *queue);
共享队列
在许多情况下, 设备驱动不需要它自己的工作队列。如果你只偶尔提交任务给队列, 简单地使用内核提供的共享的默认的队列可能更有效。若使用共享队列,就必须明白将和其他人共享它,这意味着不应当长时间独占队列(不能长时间睡眠), 并且可能要更长时间才能获得处理器。
使用的顺序:
- 建立 work_struct 或 delayed_work
static struct work_struct jiq_work;
static struct delayed_work jiq_work_delay;
INIT_WORK(&jiq_work, jiq_print_wq);
INIT_DELAYED_WORK(&jiq_work_delay, jiq_print_wq);
- 提交工作
int schedule_work(&jiq_work); /*对于work_struct结构*/
int schedule_delayed_work(&jiq_work_delay, delay); /*对于delayed_work结构*/
/*返回值的定义和 queue_work 一样*/
若需取消一个已提交给工作队列入口项, 可以使用 cancel_delayed_work和cancel_work_sync, 但刷新共享队列需要一个特殊的函数:
void flush_scheduled_work(void);
因为不知道谁可能使用这个队列,因此不可能知道 flush_schduled_work 返回需要多长时间。